The title is a little long, but this really is an interesting problem. Consider this code:
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();
}
}
First, guess the output. The s on line 7 and this.s on line 8 refer to the same variable, the static final variable s. Even without knowing the answer, we would expect the two outputs to match. But the actual output is:
true false
Let's disassemble the generated bytecode with javap. Here is the test() portion:
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
Entry #7 in the constant pool is:
#7 = String #8 // ab #8 = Utf8 ab
The first true should be familiar. When compiling source into class bytecode, the compiler replaces final constants used in the current class's methods with literals. Thus String ab1 = s + "b"; on Java source line 6 becomes String ab1 = "a" + "b"; and, since this concatenates two literals, the compiler performs the concatenation itself. The statement ultimately becomes equivalent to String ab1 = "ab";. Both cmp and ab1 therefore point to the string "ab" in the constant pool, so cmp == ab1. In the disassembled bytecode, lines 0 and 3 are identical: both ldc (Load Constant) instructions take #7 as their argument.
Bytecode lines 8–15 prepare ab2. They perform a dynamic method invocation of makeConcatWithConstants, a Java bootstrap method that handles String concatenation with “+”. This method creates a new string on the heap, which is why ab2 != cmp.
BTW, makeConcatWithConstants was introduced in JDK 9 to handle string “+” operations. Before JDK 8, javac had always used StringBuilder for this.
What causes the difference? Clearly, it is the this keyword. During compilation, Java implicitly adds a reference to the current instance, this, to every instance method. In bytecode, this is passed as a method argument. That is why, although void() has no parameters, args_size on bytecode line 5 is 1. For a variable accessed through an object reference, whether a class variable or an instance variable, the Java compiler simply disables this optimization wholesale. Change this.s here to Test.s and the output becomes true.
