Skip to content

名字起的有点长了,但是这确实是个挺有趣的问题。如下代码:

java
public class Test {
    final static String s = "a";

    public void test() {
        String cmp = "ab";
        String ab1 = s + "b";
        String ab2 = this.s + "b";
        System.out.println(ab1 == cmp);
        System.out.println(ab2 == cmp);
    }

    public static void main(String[] args) {
        new Test().test();
    }
}

我们先来猜一猜输出结果,第 7 行的 s 和第 8 行的 this.s 都是指的同一个变量,就是那个 final 静态变量 s。那么就算我们不知道结果,这两个输出应该也是一样的。但实际上的输出是

shell
true
false

我们先用 javap 将生成的字节码反汇编出来,test() 部分如下:

shell
  public void test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: ldc           #7                  // String ab
         2: astore_1
         3: ldc           #7                  // String ab
         5: astore_2
         6: aload_0
         7: pop
         8: ldc           #11                 // String a
        10: invokedynamic #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
        15: astore_3
        16: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload_2
        20: aload_1
        21: if_acmpne     28
        24: iconst_1
        25: goto          29
        28: iconst_0
        29: invokevirtual #23                 // Method java/io/PrintStream.println:(Z)V
        32: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
        35: aload_3
        36: aload_1
        37: if_acmpne     44
        40: iconst_1
        41: goto          45
        44: iconst_0
        45: invokevirtual #23                 // Method java/io/PrintStream.println:(Z)V
        48: return

常量表中的 #7 为:

shell
   #7 = String             #8             // ab
   #8 = Utf8               ab

我们先来看第一个 true,这个大家应该基本都知道原因。编译器在将源码编译为 class 字节码文件时,会将当前类的方法中出现的 final 常量替换为字面量,于是 Java 代码第 6 行的 String ab1 = s + "b"; 就变为 String ab1 = "a" + "b";,进一步,由于 ab1 是由两个字面量直接拼接的,编译器就直接帮其完成拼接,最终的结果,这条语句等价于 String ab1 = "ab";。于是 cmp 和 ab1 都指向常量池的"ab"字符串,所以 cmp == ab1。反汇编的字节码中,第 0 行和第 3 行是完全一样的,ldc(Load Constant)的参数都是 #7。

字节码的 8~15 行是准备字符串 ab2 的过程,可以看到这里执行了一个动态方法调用,调用的是方法 makeConcatWithConstants,这个方法是 Java 的一个引导方法,用于处理 Java 中对 String 进行 “+” 拼接。这个方法会在堆中创建一个新的字符串变量,这就是 ab2 != cmp 的原因。

BTW,makeConcatWithConstants 这个方法在 JDK 9 中被引入用于处理字符串“+”操作,在 JDK 8 之前,javac 一直是用 StringBuilder 类来处理的。

那么是什么导致了这个差异呢?显然,问题在这个 this 关键字上。Java 在编译时,会隐式在所有成员方法中添加一个指向当前实例的引用 this,这个 this 在字节码中是作为方法参数传递给方法的,这就是为什么void() 方法没有参数,字节码第五行中 args_size 却等于 1。对于一个对象引用的变量(无论是类变量还是成员变量),Java 编译器仅仅是很粗暴地关闭了这个优化。如果将这里的 this.s 改为 Test.s,那么这里输出的结果就是 true 了。

如有转载或 CV 请标注本站原文地址