Search code examples
javareflectionstaticfinal

final causes static behavior somehow


I have a code like this:

public class App {
    private final String some;
    public App(){
        some = "old";
    }
    public static void main(String... args) throws NoSuchFieldException, IllegalAccessException {
        App a = new App();
        a.magic();
        System.out.println(a.some);

    }
    private void magic() throws NoSuchFieldException, IllegalAccessException {
        Field field = this.getClass().getDeclaredField("some");
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(this, "new");
        String someDuplicate = (String) field.get(this);
        System.out.println(someDuplicate);
    }
}

the output from this will be

new 
new

but if i'll change variable initialization to this:

private final String some = "old";

the output will be

new
old

seems like inline initialization causes static-like behavior of final non-static field

I could'n find any dock reference to this behavior, may be there is some logical explanation to this.

By the way this way to init field causes behavior like in constructor init case:

{
    some = "old";
}

Solution

  • The javac does constant inlining. When you have a code such as

    class A {
        final String text = "Hello";
    
        public static void main(String... args) {
            System.out.println(new A().text);
        }
    }
    

    The javac can inline the constant as it is known at compile time. This makes changing the underlying field have no effect on places it has been inlined.

    By moving the value to the constructor, it is no longer known at compile time.

    Dumping the byte code for the main method you can see it doesn't read the field but rather LDC loads the constant "Hello"

      public static varargs main([Ljava/lang/String;)V
       L0
        LINENUMBER 5 L0
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        NEW A
        DUP
        INVOKESPECIAL A.<init> ()V
        INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
        POP
        LDC "Hello"
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L1
        LINENUMBER 6 L1
        RETURN
       L2
        LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
        MAXSTACK = 3
        MAXLOCALS = 1
    

    What I find interesting is that it still creates the A and checks it for null using .getClass() so it's optimisation only go so far.

    BTW You can work around this without using a constructor/initialisation block with a wrapping method.

    class A {
        final String text = dynamic("Hello");
        // or final String text = String.valueOf("Hello");
    
        public static void main(String... args) {
            System.out.println(new A().text);
        }
    
        static <T> T dynamic(T t) {
            return t;
        }
    }
    

    or any expression it can't determine at compile time.