Changing private final fields via reflection
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.
Alexandr
Updated on July 09, 2022Comments
-
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 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 modifyfinal
variables after they have been set. -
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
inreturn "s = " + s;
with the compile-time constant. This can be demonstrated by making two classes,A
andB
so thatA
refers to a constant defined inB
. Now, changing the constant inB
and recompiling onlyB
will keep the old constant inA
! Sneaky, but true. -
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 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 over 13 yearsI really like this clear explanation and
intern()
approach, did not know about that before... Thanks. -
Rafael Winterhalter over 10 yearsActually, rather use
toString
since it avoids a lookup in the string pool. -
Lutosław over 7 yearsShould I call
f.setAccessible(true);
after that to restore field modifiers? -
Jiri Patera over 7 yearsI 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 originalaccessible
flag state later on in your code, you don't have to keep and then restore its state.