How Java’s this Keyword Can Prevent Compile-Time Constant Propagation

How Java’s this Keyword Can Prevent Compile-Time Constant Propagation

In Java, using `this` can prevent the compiler from optimizing constants. Although `ab1` and `ab2` in this example appear to refer to the same static final variable `s`, comparing them produces different results. `ab1` concatenates a direct reference to the static variable, while `ab2` accesses it through `this`, preventing the same constant propagation optimization. This illustrates how a small syntactic difference can change compilation behavior.

The title is a little long, but this really is an interesting problem. Consider this code:

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

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:

shell
true
false

Let's disassemble the generated bytecode with javap. Here is the test() portion:

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

Entry #7 in the constant pool is:

shell
   #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.

Deep Copying Between Different Struct Types in Go
Setting Up the CSAPP Lab Environment