The Issue of `this` Keyword Causing Compile-Time Constant Propagation Optimization Failure in Java
The title is a bit long, but this is indeed a rather intriguing problem. Take a look at the following 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();
}
}
Let’s try to guess the output first. Both s
on line 7 and this.s
on line 8 refer to the same variable—the final static variable s
. So even if we don’t know the exact result, these two print statements should logically produce the same output. But the actual output is:
true
false
Let’s take a look at the bytecode disassembled using javap
, focusing on the test()
method:
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
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
The constant pool entry for #7 is:
#7 = String #8 // ab
#8 = Utf8 ab
Let’s analyze the first true
output—this should be familiar to most. When the compiler translates the source code to class bytecode, it replaces the final constants within the current class methods with their literal values. Thus, the Java code at line 6:
String ab1 = s + "b";
effectively becomes:
String ab1 = "a" + "b";
Since both are string literals, the compiler concatenates them at compile-time, making the statement equivalent to:
String ab1 = "ab";
As a result, cmp
and ab1
both reference the same string "ab"
in the constant pool, so cmp == ab1
evaluates to true
. This is why in the bytecode at lines 0 and 3, the ldc
(Load Constant) instructions reference the exact same constant pool entry #7.
Now, lines 8 through 15 prepare the string for ab2
. Here, we see a dynamic method invocation via invokedynamic
, calling the method makeConcatWithConstants
. This is a bootstrap method in Java that handles string concatenation with "+"
. This method creates a new String object on the heap, which explains why ab2 != cmp
—they point to different objects.
By the way, makeConcatWithConstants
was introduced in JDK 9 to optimize string concatenation. Before JDK 9, javac
used the StringBuilder
class to handle "+"
operations on strings.
So what causes this difference? Clearly, the culprit is the this
keyword. During compilation, Java implicitly adds a reference to the current instance (this
) in all member methods. This this
reference is passed as a hidden parameter in the bytecode for the method. That’s why even though the method signature is void ()
, the bytecode’s args_size
value is 1 at line 5.
For an object reference variable (whether a class variable or member variable), the Java compiler simply disables the constant propagation optimization in this context. If you replace this.s
with Test.s
, the output becomes true
as expected.