Changing private final fields via reflection

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = " + s;
    }
}
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you’re not!");
System.out.println(pf);
System.out.println(f.get(pf));

Output:

s = I’m totally safe
f.get(pf): I’m totally safe
s = I’m totally safe
No, you’re not!

Why does it work by this way, can you please explain? The first print tells us that the private "s" field has not been changed, as I expect. But if we get the field via reflection, the second print shows, it is updated.


Solution 1:

This answer is more than exhaustive on the topic.

JLS 17.5.3 Subsequent Modification of Final Fields

Even then, there are a number of complications. If a final field is initialized to a compile-time constant in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the compile-time constant.

But, if you read the paragraph above very carefully, you may find a way around here (set the private final field in the constructor instead of in the field definition):

import java.lang.reflect.Field;


public class Test {

  public static void main(String[] args) throws Exception {
    WithPrivateFinalField pf = new WithPrivateFinalField();
    System.out.println(pf);
    Field f = pf.getClass().getDeclaredField("s");
    f.setAccessible(true);
    System.out.println("f.get(pf): " + f.get(pf));
    f.set(pf, "No, you’re not!");
    System.out.println(pf);
    System.out.println("f.get(pf): " + f.get(pf));
  }

  private class WithPrivateFinalField {
    private final String s;

    public WithPrivateFinalField() {
      this.s = "I’m totally safe";
    }
    public String toString() {
      return "s = " + s;
    }
  }

}

The output is then as follows:

s = I’m totally safe
f.get(pf): I’m totally safe
s = No, you’re not!
f.get(pf): No, you’re not!

Hope this helps a bit.

Solution 2:

This

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = " + s;
    }  
} 

actually compiles like this:

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = I’m totally safe";
    }  
}

That is, compile-time constants get inlined. See this question. The easiest way to avoid inlining is to declare the String like this:

private final String s = "I’m totally safe".intern();

For other types, a trivial method call does the trick:

private final int integerConstant = identity(42);
private static int identity(int number) {
    return number;
}

Solution 3:

Here's a decompile of WithPrivateFinalField class file (I put it in a separate class for simplicity):

  WithPrivateFinalField();
     0  aload_0 [this]
     1  invokespecial java.lang.Object() [13]
     4  aload_0 [this]
     5  ldc <String "I’m totally safe"> [8]
     7  putfield WithPrivateFinalField.s : java.lang.String [15]
    10  return
      Line numbers:
        [pc: 0, line: 2]
        [pc: 4, line: 3]
        [pc: 10, line: 2]
      Local variable table:
        [pc: 0, pc: 11] local: this index: 0 type: WithPrivateFinalField

  // Method descriptor #22 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  public java.lang.String toString();
    0  ldc <String "s = I’m totally safe"> [23]
    2  areturn
      Line numbers:
        [pc: 0, line: 6]
      Local variable table:
        [pc: 0, pc: 3] local: this index: 0 type: WithPrivateFinalField

Note in the toString() method, the constant used at address 0 [0 ldc <String "s = I’m totally safe"> [23]] shows the compiler already concatenated the string literal "s = " and the private final field " I’m totally safe" together in advance and stored it. The toString() method will always return "s = I’m totally safe" regardless of how the instance variable s changes.

Solution 4:

Being final, the compiler expected the value not to change, so it probably hardcoded the string directly into your toString method.