澄清 StringBuilder 参考和方法执行顺序

Clarification on StringBuilder reference and methods execution order

这段代码

    StringBuilder b1=new StringBuilder("hello");
    b1.append(b1.append("!"));
    System.out.println("b1 = "+b1);

将打印

    b1 = hello!hello!

因为内部append先执行,修改了对象b1;然后评估外部 b1 (现在等于 hello! )并将相同的字符串附加到它。所以

  1. 执行内部表达式
  2. 原始对象被修改
  3. 外部表达式在修改后的对象上执行

但是现在,为什么这段代码会抛出一个 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");

并打印

    java.lang.NullPointerException
    s1 = world!
    s2 = world!

我期望引用 s1 指向 s2 外部 append 被执行之前引用的对象。

在某种程度上,分配 b1.append("!"); 会影响 "outer" b1,但 s1=s2.append("!") 不会。我知道这是因为在第一种情况下我正在修改对象,而在第二种情况下我正在修改引用但是...... values/references/methods 的评估和执行顺序是什么?

编辑

同样的事情发生在数组上:

    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");

打印

    y = [2, 0, 4]

同时

    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));

打印

    java.lang.NullPointerException
    x1 = [1, 2, 3]
    x2 = [1, 2, 3]

in b1.append(b1.append("!")); 不是内部追加它首先执行。 Java 将首先调用 append,然后将为将修改 b1 对象的此调用评估参数 b1.append("!")。这里s1.append(s1=s2.append("!"));会调用 s1.append() 方法,但因为 s1 它为空,将抛出 NullPointerException.

这是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.

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.

所以在 s1.append(s1=s2.append("!")); 行中 s1(在 .append(s1 = ...) 之前)首先在参数表达式 s1=s2.append("!") 之前求值。因此,在 s1 更改为引用 StringBuilder s2 实例之前,null 引用被记住为目标引用。

然后计算参数表达式,因此执行 s1=s2.append("!")。但是它记住了之前的目标引用,所以在null引用上调用了append方法,调用的结果抛出一个NullPointerException.

让我们看一下您示例中的字节码,

   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

如果你看过我在代码中的注释,你就会知道null被加载用于调用方法append()

再举个例子,

    StringBuilder s1 = new StringBuilder();
    StringBuilder s2 = new StringBuilder("world");
    s1.append(s1 = s2.append("!"));
    System.out.println(s1);

这只会打印 world!。即使您期望 world!world!.

那是因为在为方法调用加载后,您正在重新设置 s1 的值。这意味着在方法调用中重新分配的值将被覆盖。

发生的情况是 Java 解释器首先尝试定位(不是求值,只是定位)方法,在本例中为 s1.append()。我的猜测是它这样做是为了将方法指针添加到堆栈。为此,它需要知道对象 s1 的确切 class,因此它取消引用它。因为 s1 为空,所以这会导致 NullPointerException.

这甚至发生在对参数求值之前,因此 s1 仍然是 null

This SO answer 列出了我们 s1.append 调用中发生的不同步骤:

  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.

NullPointerException 发生在第 1 步。