benf.org :  other :  cfr :  Decompilation of final statics.

Final statics are completely inlined

It's a bit annoying to see a complete disconnect between decompiled usages of final statics and the original code.

eg:

public class FinalVarTest4 {
    public static final String h1 = "Hello";
    public static final String h2 = "Chap";

    public void thing(){
        System.out.println(h1);
        System.out.println(h2);
        System.out.println(h2.equals(h1));
    }
}
will decompile (in CFR 0_124) to
public class FinalVarTest4 {
    public static final String h1 = "Hello";
    public static final String h2 = "Chap";

    public void thing() {
        System.out.println("Hello");
        System.out.println("Chap");
        System.out.println("Chap".equals("Hello"));
    }
}

At this point it looks like CFR has lost track of the original statics, which it has!

This compilation behaviour is defined in §13.4.9 of the JLS -

If a field is a constant variable (§4.12.4), and moreover is static, then deleting the keyword final or changing its value will not break compatibility with pre-existing binaries by causing them not to run, but they will not see any new value for a usage of the field unless they are recompiled. This result is a side-effect of the decision to support conditional compilation (§14.21). (One might suppose that the new value is not seen if the usage occurs in a constant expression (§15.28) but is seen otherwise. This is not so; pre-existing binaries do not see the new value at all.)

§14.21 goes on to say:

Conditional compilation comes with a caveat. If a set of classes that use a "flag" variable - or more precisely, any static constant variable (§4.12.4) - are compiled and conditional code is omitted, it does not suffice later to distribute just a new version of the class or interface that contains the definition of the flag. The classes that use the flag will not see its new value, so their behavior may be surprising. In essence, a change to the value of a flag is binary compatible with pre-existing binaries (no LinkageError occurs) but not behaviorally compatible.

Another reason for "inlining" values of static constant variables is because of switch statements. They are the only kind of statement that relies on constant expressions, namely that each case label of a switch statement must be a constant expression whose value is different than every other case label. case labels are often references to static constant variables so it may not be immediately obvious that all the labels have different values. If it is proven that there are no duplicate labels at compile time, then inlining the values into the class file ensures there are no duplicate labels at run time either - a very desirable property.

The upshot of the two above excerpts is that any final static values are completely inlined, and from a bytecode point of view, impossible to differentiate from explicit use of those constants - even in other classes - your class using 3rd party constants will have no visible reference to the third party class.

(The second paragraph ties up nicely with the rigamarole javac goes to to compile a switch on an enum)

Final statics and constant folding

I mention constant folding elsewhere. It's (predictable, but) interesting to see the effect of combining constant folding with final statics. (I'm using integers below as they make for simpler bytecode, but exactly the same is true for other types, e.g. strings).

public class FinalVarTest {
    public static final int THREE = 3;
    public static final int FOUR = 4;

    public void thing() {
        System.out.println(12);
        System.out.println(THREE * FOUR);
    }
}
has the following bytecode
  public void thing();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        12
       5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: bipush        12
      13: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      16: return
... as expected, the constants have been inlined, then folded.

So, this is annoying, can it be worked around?

Not really! Or, at least, not perfectly, by a long chalk. It's highly impractical (and almost certainly wrong) to assume that just because someone declares

final static MAX_VISITORS = 3;
somewhere in a jar, that all usages of the value 3 came from there.

... especially when we could have multiple constants defined to have the same value! Which do you choose?

It's actually a bit worse than that, as even with one constant locally defined, we can refer to external constants with the same value

class FinalVarConstants {
    public static final String[] Months = new String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
    public static final int DECEMBER = 11;
}

public class FinalVarTest5 {
    public static final int MAX_CUSTOMERS = 11;

    public void thing() {
        System.out.println(MAX_CUSTOMERS);
        System.out.println(FinalVarConstants.DECEMBER);
        System.out.println(FinalVarConstants.Months[FinalVarConstants.DECEMBER]);
    }
}

decompiles to

public class FinalVarTest5 {
    public static final int MAX_CUSTOMERS = 11;

    public void thing() {
        System.out.println(11);
        System.out.println(11);
        System.out.println(FinalVarConstants.Months[11]);
    }
}

Of note here is that just because Months is static final, it's not inlined, becuase it's not a constant variable. (A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28).).

We definitely *don't* want to naively decompile this as

public class FinalVarTest5 {
    public static final int MAX_CUSTOMERS = 11;

    public void thing() {
        System.out.println(MAX_CUSTOMERS);
        System.out.println(MAX_CUSTOMERS);
        System.out.println(FinalVarConstants.Months[MAX_CUSTOMERS]);
    }
}

The issue here is that integer constants are just too common, and I can't really reliably assume they originate from specific static final fields.

Ok, can we do anything?

Matti suggested it would be nice to be able to recover String constant usage. There are lots of caveats (below)... however, compared to integer constants, String constants (*sticks finger in air - this is a heuristic, as are many behaviours of CFR!*) are less likely to collide with other useful values.

As such, from 0_125 on, CFR will check literal strings in code to see if they are identical to string constants defined in that class, or outer classes.

Meaning that we recover...

public class FinalVarTest4 {
    public static final String h1 = "Hello";
    public static final String h2 = "Chap";

    public void thing() {
        System.out.println(h1);
        System.out.println(h2);
        System.out.println(h2.equals(h1));
    }
}

Caveats

I don't like it!

Turn it off with --relinkconststring false


Last updated 01/2018