class WithPrivateFinalField {
private final String s = \"I’m totally safe\";
public String toString() {
return \"s = \" + s;
}
}
WithPrivateFinal
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.
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;
}
Being final
, the compiler expected the value not to change, so it probably hardcoded the string directly into your toString
method.
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.