This piece of code
StringBuilder b1=new StringBuilder("hello");
b1.append(b1.append("!"));
System.out.println("b1 = "+b1);
will print
b1 = hello!hello!
because the inner append
is executed first and modifies the object b1
; then the outer b1
is evaluated (it is equal to hello!
now) and the same string is appended to it. So
But now, why does this code throw a NullPointerException
?
StringBuilder s1=null;
StringBuilder s2=new StringBuilder("world");
try{s1.append(s1=s2.append("!"));}
catch(Exception e){System.out.println(e);}
System.out.println("s1 = "+s1+"\ns2 = "+s2+"\n");
and prints
java.lang.NullPointerException
s1 = world!
s2 = world!
I was expecting the reference s1
to be pointing at the object referenced by s2
before the outer append
gets executed.
In some way, assigning b1.append("!");
affects the "outer" b1
, but s1=s2.append("!")
doesn't. I know it's due to the fact that in the first case I'm modifing the object, while in the second I'm modifing the reference but... what's the order in which values/references/methods are evaluated and executed?
Same thing happens with arrays:
int[] y = { 0, 0, 0 };
try {y[y[0] = 2] = 4;}
catch (Exception e) {System.out.println(e);}
System.out.println("y = "+Arrays.toString(y)+"\n");
prints
y = [2, 0, 4]
while
int[] x1 = null;
int[] x2 = { 1, 2, 3 };
try {x1[(x1=x2)[0]] = 0;}
catch (Exception e) {System.out.println(e);}
System.out.println("x1 = "+Arrays.toString(x1)+"\nx2 = "+Arrays.toString(x2));
prints
java.lang.NullPointerException
x1 = [1, 2, 3]
x2 = [1, 2, 3]
This is specified in the JLS 15.12.4.:
If form is ExpressionName . [TypeArguments] Identifier, then:
If the invocation mode is static, then there is no target reference. The ExpressionName is evaluated, but the result is then discarded.
Otherwise, the target reference is the value denoted by ExpressionName.
and
As part of an instance method invocation (§15.12), there is an expression that denotes the object to be invoked. This expression appears to be fully evaluated before any part of any argument expression to the method invocation is evaluated.
So in the line s1.append(s1=s2.append("!"));
s1
(before .append(s1 = ...)
) is evaluated first before the argument expression s1=s2.append("!")
. So the null
reference is remembered as the target reference before s1
is changed to refer to the StringBuilder s2
instance.
Then the argument expression is evaluated so s1=s2.append("!")
is executed. But it remembered the target reference before, so the append
method is invoked on the null
reference, and the result of the invocation throws a NullPointerException
.
Let's take a look at the byte code in your example,
0: aconst_null
1: astore_1
// Comment: null is stored to s1.
2: new #18 // class java/lang/StringBuilder
5: dup
6: ldc #20 // String world
8: invokespecial #22 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
11: astore_2
// Comment: new StringBuilder is stored to s2.
12: aload_1
// Comment: s1 (which is null) is loaded for method call.
13: aload_2
// Comment: s2 is loaded for method call.
14: ldc #25 // String !
16: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: dup
20: astore_1
// Comment: s2.append() return value is stored in s1.
21: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;
// Comment: append() method is called on already loaded s1 value (which is null).
24: pop
25: return
If you read through my comments in the code, you will know that null
is loaded for invoking the method append()
.
Let's take another example,
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder("world");
s1.append(s1 = s2.append("!"));
System.out.println(s1);
This will only print world!
. Even though you would expect world!world!
.
That's because you are reassining the value of s1
after it's loaded for method call. Which means on the method call the reassigned value will be overwritten.
What happens is that the Java interpreter first tries to locate (not evaluate, just locate) the method, in this case s1.append()
. My guess is that it does it to add the method pointer to the stack. To do this, it needs to know the exact class of the object s1
, so it dereferences it. Because s1
is null, this results in the NullPointerException
.
This happens even before the arguments are evaluated, hence the fact that s1
is still null
.
This SO answer lists the different steps that occur in our s1.append
call :
The object pointer is used to reference the object, and from there the Class object.
The method pointer is located in the Class object. (The lookup to convert method name to method index was largely done when the class was loaded, so this is basically just an array index operation.)
Generally some sort of a "mark" is pushed onto the JVM stack. This would contain the caller's instruction pointer, and a pointer to the base of his stack. (Lots of different implementations here.)
The method's definition is consulted to see how many local vars are needed. That many blank elements are pushed onto the stack.
The object ("this") pointer is stored in local var 0, and any parms are stored in 1,2,3... as appropriate.
Control is transferred to the called method.
The NullPointerException
occurs at step 1.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With