Java puzzler: String concatenation

A few days ago at work I wrote a toString() method for a complex class that did not work as I expected. To protect against a NullPointerException I used a ternary operator so I include one String if the field was null and a different String if it was not. To figure out why this didn’t work as I expected I boiled down the problem to the simplest possible example I could think of and came up with the following.

public class Item {
  private Integer alpha;

  public Item(Integer alpha) { this.alpha = alpha; }

  @Override
  public String toString() {
    return "Item{alpha="
      + alpha == null ? "invalid" : alpha.toString()
      + "}";
  }
}

public class Main {
  public static void main(String[] args) {
    Item i = new Item(3);
    System.out.println(i);
  }
}

I’d expect the result of toString() to be Item{alpha=3} but what it actually returns is 3}. Worse, if alpha is null, then toString() throws a NullPointerException.

To see why toString() prints 3} you can disassemble the Item class and see the bytecode.

public java.lang.String toString();
    Code:
       0: new           #3                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
       7: ldc           #5                  // String Item{alpha=
       9: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      12: aload_0
      13: getfield      #2                  // Field alpha:Ljava/lang/Integer;
      16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      19: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: ifnonnull     30
      25: ldc           #9                  // String invalid
      27: goto          55
      30: new           #3                  // class java/lang/StringBuilder
      33: dup
      34: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      37: aload_0
      38: getfield      #2                  // Field alpha:Ljava/lang/Integer;
      41: invokevirtual #10                 // Method java/lang/Integer.toString:()Ljava/lang/String;
      44: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      47: ldc           #11                 // String }
      49: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      52: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      55: areturn

In the first several lines a StringBuilder is constructed and the String constant Item{alpha= is added to it. Then, at labels 13-19, alpha is loaded to the top of the stack, StringBuilder.append() is invoked (the parameter to append() is what is on the top of the stack, alpha), and StringBuilder.toString() is called. All of these will complete successfully because StringBuilder.append() is null safe.

This leaves a String at the top of the stack. This is important because at label 22, ifnonnull checks if the item at the top of the stack is null or not. It will never be null, so jump to label 30 and a second StringBuilder is constructed. Then at 38, alpha is pushed to the top of the stack and at 41 Integer.toString() is invoked. (So if alpha is null this will throw the NullPointerException.) If alpha is not null then the result of Integer.toString() is now at the top of the stack. It is added to the second StringBuilder followed by the String constant } and the then the result of StringBuilder.toString() (on the second StringBuilder) is returned.

Given this bytecode I understand why the results are what they are.

But why does the compiler generate this specific bytecode? Because the string concatenation operator (+) has a higher precedence than the equality operator (==). So the code that is written is equivalent to the following java:

public String toString() {
  return ("Item{alpha=" + alpha) == null ? "invalid" : alpha.toString()
    + "}";
}

This is exactly what the bytecode is producing! Now the NullPointerException makes perfect sense to me. The String "Item{alpha=" + alpha will never be null, regardless of the value of alpha, so alpha.toString() will always be invoked.

Using this same grouping, it is also clear why this code fragment does not compile if you change the condition used in the ternary operator to an inequality.

public String toString() {
  return "Item{alpha="
    + alpha > 0 ? "invalid" : alpha.toString()
    + "}";
}
$ javac Item.java
Item.java:8: error: bad operand types for binary operator '>'
      + alpha > 0 ? "invalid" : alpha.toString()
              ^
  first type:  String
  second type: int
1 error

By the way, it is easy to make the code work as originally expected. All you need to do is wrap the ternary expression in parentheses.

@Override
public String toString() {
  return "Item{alpha="
    + (alpha == null ? "invalid" : alpha.toString())
    + "}";
}
$ java Main
Item{alpha=3}

So beware the precedence of operators. Depending on how you format your code you may trick yourself into thinking that it is doing one thing when it is really doing something else.

For this post I am using Java 1.8.0_131, but the results should be the same for all versions of Java.

CC By SA Cover photo