Changing private final fields via reflection

52,914

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.

Share:
52,914
Alexandr
Author by

Alexandr

Updated on July 09, 2022

Comments

  • Alexandr
    Alexandr almost 2 years
    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.

  • Stephen C
    Stephen C over 13 years
    +1 - this is the real answer. The OP's program is causing the JIT compiler's assumption that s doesn't change to be invalid. This is not a compiler bug, since the JLS explicitly warns that "bad things" like this can happen if you modify final variables after they have been set.
  • Joonas Pulakka
    Joonas Pulakka over 13 years
    @Stephen C: Actually the behavior has nothing to do with JIT compiling. It's already the bytecode compiler that replaces the s in return "s = " + s; with the compile-time constant. This can be demonstrated by making two classes, A and B so that A refers to a constant defined in B. Now, changing the constant in B and recompiling only B will keep the old constant in A! Sneaky, but true.
  • Stephen C
    Stephen C over 13 years
    @Joonas - ok ... but it would also be legitimate for the JIT compiler to do this kind of optimization.
  • Joonas Pulakka
    Joonas Pulakka over 13 years
    @Stephen C: Indeed. IMO it would be way better if the JIT compiler would do that instead of the bytecode compiler. With the current behavior, you can break someone else's class just by modifying the value of a, say, flag constant in your class.
  • Jiri Patera
    Jiri Patera over 13 years
    I really like this clear explanation and intern() approach, did not know about that before... Thanks.
  • Rafael Winterhalter
    Rafael Winterhalter over 10 years
    Actually, rather use toString since it avoids a lookup in the string pool.
  • Lutosław
    Lutosław over 7 years
    Should I call f.setAccessible(true); after that to restore field modifiers?
  • Jiri Patera
    Jiri Patera over 7 years
    I think you don't have to restore the field accessibility as it is in fact a copy of some Java internal Field instance. Such a copy is just for you to use. So if you don't need to read the original accessible flag state later on in your code, you don't have to keep and then restore its state.