Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clarification on StringBuilder reference and methods execution order

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

  1. inner expression is executed
  2. original object gets modified
  3. outer expression is executed on modified object

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?

Edit

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]
like image 580
Luigi Cortese Avatar asked Jul 26 '15 09:07

Luigi Cortese


3 Answers

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.

like image 136
Alexis C. Avatar answered Sep 20 '22 16:09

Alexis C.


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.

like image 42
Codebender Avatar answered Sep 22 '22 16:09

Codebender


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 :

  1. The object pointer is used to reference the object, and from there the Class object.

  2. 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.)

  3. 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.)

  4. The method's definition is consulted to see how many local vars are needed. That many blank elements are pushed onto the stack.

  5. The object ("this") pointer is stored in local var 0, and any parms are stored in 1,2,3... as appropriate.

  6. Control is transferred to the called method.

The NullPointerException occurs at step 1.

like image 43
David Avatar answered Sep 20 '22 16:09

David