How "final" is final?

Musings about finality in the Java language and VM

Volker Simonis [Фолкер Симонис], SAP / volker.simonis@gmail.com

 

Java's definition of “final”

“A final variable may only be assigned to once..”

“Once a final variable has been assigned, it always contains the same value.”

            
class Test0 {
  private final int f;
  public Test0() {
  }
}
            
          

Compile time error: variable f might not have been initialized.

            
class TestA {
  private final int f = 0;
  public TestA(int f) {
    this.f = f;
  }
}
            
          

Compile time error: cannot assign a value to final variable f

            
class TestB {
  private final int f;
  {
    f = 0; // Instance initializer
  }
  public TestB(int f) {
    this.f = f;
  }
}
            
          

Compile time error: cannot assign a value to final variable f

            
class TestC {
  private final int f;
  public TestC(int f) {
    this.f = f;
  }
}
            
          

OK: since Java 1.1 (“blank final”).

            
class TestF {
  private final int f;
  public TestF(int f) {
    if (f > 0) {
      this.f = f;
    } else {
      this.f = 42;
    }
  }
}
            
          

OK: javac does a certain amount of flow analysis..

            
 0) {
      this.f = f;
    }
    if (f <= 0) {
      this.f = 42;
    }
  }
}
]]>
            
          

..but not too much!

Compile time error: variable f might already have been initialized.

            
class TestG1 {
  private final int f;
  public TestG1(int f) {
    try {
      this.f = f;
    }
    catch (Exception e) {

    }
  }
}
            
          

Compile time error: variable f might not have been initialized.

            
class TestG1 {
  private final int f;
  public TestG1(int f) {
    try {
      this.f = f;
    }
    catch (Exception e) {
      this.f = 42;
    }
  }
}
            
          

Compile time error: variable f might already have been initialized.

                
class TestG1 {
  public int f;
  public TestG1(int f) {
    try {
      this.f = f;
    }
    catch (Exception e) {
      this.f = 42;
    }
  }
}
                
              

Without final OK

                

    4: aload_0
    5: iload_1
    6: putfield      #2 // Field f:I
    9: goto          19
   12: astore_2
   13: aload_0
   14: bipush        42
   16: putfield      #2 // Field f:I
   19: return
 Exception table:
    from    to  target type
       4     9    12   Exception
]]>
                
              

                
class TestG1 {
  public final int f;
  public TestG1(int f) {
    try {
      this.f = f;
    }
    catch (Exception e) {
      this.f = 42;
    }
  }
}
                
              

With final compile Error!

But we can generate the Bytecode:

                

    4: aload_0
    5: iload_1
    6: putfield      #2 // Field f:I
    9: goto          19
   12: astore_2
   13: aload_0
   14: bipush        42
   16: putfield      #2 // Field f:I
   19: return
 Exception table:
    from    to  target type
       4     9    12   Exception
]]>
                
              

The JVM's definition of “final”

“If the field is final the putfield instruction must occur in
an instance initialization method (<init>) of the current class.”

No or multiple assignmets within constructors are possible according to the JVMS.

Using jdk.internal.org.objectweb.asm

            
", "(I)V", null, null);
  Label l_try_beg = new Label(), l_try_end = new Label();
  Label l_catch = new Label(), l_end = new Label();
  constr.visitCode();
  constr.visitVarInsn(ALOAD, 0);
  constr.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V");
  constr.visitLabel(l_try_beg);
  constr.visitVarInsn(ALOAD, 0);
  constr.visitVarInsn(ILOAD, 1);
  constr.visitFieldInsn(PUTFIELD, "TestG1", "f", "I");
  constr.visitLabel(l_try_end);
  constr.visitJumpInsn(GOTO, l_end);
  constr.visitLabel(l_catch);
  constr.visitVarInsn(ASTORE, 2);
  constr.visitVarInsn(ALOAD, 0);
  constr.visitIntInsn(BIPUSH, 42);
  constr.visitFieldInsn(PUTFIELD, "TestG1", "f", "I");
  constr.visitLabel(l_end);
  constr.visitInsn(RETURN);
  constr.visitTryCatchBlock(l_try_beg, l_try_end, l_catch, "java/lang/Exception");
  // max stack and max locals are automatically computed (because of the
  // 'ClassWriter.COMPUTE_FRAMES' option) in the ClassWriter constructor,
  // but we need this call nevertheless in order for the computation to happen!
  constr.visitMaxs(0, 0);
  constr.visitEnd();

  // Get the bytes of the class..
  byte[] b = cw.toByteArray();
  // ..and write them into a class file (for debugging)
  FileOutputStream fos = new FileOutputStream("TestG1.class");
  fos.write(b);
  fos.close();

  // Load the newly created class..
  Final f = new Final();
  Class testG1Class = f.defineClass("TestG1", b, 0, b.length);
  // ..get the constructor..
  Constructor c = testG1Class.getConstructor(int.class);
  // ..and create a new "TestG1" object
  Object testG1 = c.newInstance(42);
  Field int_f = testG1Class.getDeclaredField("f");
  System.out.println("testG1.f = " + int_f.getInt(testG1));
}
]]>
            
          

Compile with “javac javac -XDignore.symbol.file=true

            
class TestH {
  private final int f;
  {
    foo();
  }
  public TestH(int f) {
    this.f = f;
    System.out.println(this + ".f = " + this.f);
  }
  public void foo() {
    System.out.println(this + ".f = " + this.f);
  }
}
new TestH(42);
            
          

TestH@15db9742.f = 0
TestH@15db9742.f = 42

SEI CERT: TSM01-J. Do not let the "this" reference escape during object construction!

            
class TestH2 {
  private static final int f;
  static {
    f = foo();
  }
  public TestH2() {
    System.out.println(TestH2.class + ".f = " + f);
  }
  static int foo() {
    System.out.println(TestH2.class + ".f = " + f);
    return 42;
  }
}
new TestH2();
            
          

class Final$TestH2.f = 0
class Final$TestH2.f = 42

JDK9 Bug: 8087161: Fails to start up initialize system class loader on unsupported charset

            
class TestI {
  public final int f;
  public TestI(int f) {
    this.f = f;
  }
  public void set(int f) {
    // change value of 'f'
  }
}

TestI i = new TestI(42);
i.set(99);
System.out.println("i.f = " + i.f);
            
          

i.f = 99

Question: How can we change the value of 'f' in Java?

JNI

“The JNI does not enforce class, field, and method access control restrictions that can be expressed at the Java programming language level through the use of modifiers such as private and final.”

            
class TestI {
  ...
  public native void set(int f);
  static {
    System.loadLibrary("TestI1");
  }
}
            
          
            
GetObjectClass(obj);

  jfieldID fid = env->GetFieldID(thisClass, "f", "I");

  env->SetIntField(obj, fid, val);
}
]]>
            
          

JDK9 RFE: 8058164: final fields in objects need to support inlining optimizations

Reflection

            

            
          

Method Handles

            

            
          

sun.misc.Unsafe

            
 unsafeConstructor =
        sun.misc.Unsafe.class.getDeclaredConstructor();
      unsafeConstructor.setAccessible(true);
      UNSAFE = unsafeConstructor.newInstance();
    } catch (Exception e) {}
  }
  public void set(int f) throws Exception {
    java.lang.reflect.Field field = this.getClass().getDeclaredField("f");
    UNSAFE.putInt(this, UNSAFE.objectFieldOffset(field), f);
  }
}
]]>
            
          
              

              
            

Unchanged 42
Unchanged 99
            
              

              
            
              

              
            

Unchanged 42
Changed   99
            
              

              
            

Constant Variables

“We call a variable, of primitive type or type String, that is final and
initialized with a compile-time constant expression a constant variable.”

“References to fields that are constant variables are resolved at compile time to the constant value that is denoted. No reference to such a constant field should be present in the code...”

Constant variables are inlined by javac !

J. Bloch, N. Gafter - “Java Puzzlers”, Nr. 93

                

                
              
                

                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());

Test.test.set_f(99);
System.out.println(Test.get_f());

test42.set_f(99);
System.out.println(Test.get_f());
]]>
                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 99

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());

Test.test.set_f(99);
System.out.println(Test.get_f());

test42.set_f(99);
System.out.println(Test.get_f());
]]>
                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 99

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)          // -> 42
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());

Test.test.set_f(99);
System.out.println(Test.get_f());

test42.set_f(99);
System.out.println(Test.get_f());
]]>
                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 99

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)          // -> 42
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 42

Test.test.set_f(99);
System.out.println(Test.get_f());

test42.set_f(99);
System.out.println(Test.get_f());
]]>
                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 99

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)          // -> 42
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 42

Test.test.set_f(99);
System.out.println(Test.get_f());  // -> 42

test42.set_f(99);
System.out.println(Test.get_f());
]]>
                
              
                
 42

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 99

Test test42 = new Test(42);
Test.set_test(test42);
for (int n = 0; n < 20000; n++)
  if (Test.get_f() != 42)          // -> 42
    System.out.println("!!!");

Test.set_test(new Test(99));
System.out.println(Test.get_f());  // -> 42

Test.test.set_f(99);
System.out.println(Test.get_f());  // -> 42

test42.set_f(99);
System.out.println(Test.get_f());  //  ???
]]>
                
              

With -XX:-TrustFinalNonStaticFields -> 99

              


              
            

With -XX:+TrustFinalNonStaticFields -> 42

              


              
            

JDK 7: 6912065: final fields in objects need to support inlining optimizations for JSR 292
By default only enabled for java.lang.invoke and sun.invoke.

The @Stable Annotation