2010-12-23 12 views
49
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)); 

Ausgang:Ändern privaten letzte Felder durch Reflexion

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

Warum ist es auf diese Weise arbeiten, können Sie bitte erklären? Der erste Druck sagt uns, dass das private "s" Feld nicht geändert wurde, wie ich es erwarte. Aber wenn wir das Feld über Reflexion bekommen, zeigt der zweite Druck, es wird aktualisiert.

+0

Siehe: http: // stackoverflow.com/questions/3301635/change-private-statisch-final-field-using-java-reflection/31268945 # 31268945 – iirekm

+0

http://stackoverflow.com/questions/3301635/change-private-static-final-field-using- Java-Reflexion/31268945 # 31268945 – iirekm

Antwort

68

This answer ist mehr als erschöpfend zum Thema.

JLS 17.5.3 Nachträgliche Modifizierung von Final Felder

Selbst dann gibt es eine Reihe von Komplikationen. Wenn ein letztes Feld auf eine Kompilierzeitkonstante in der Felddeklaration initialisiert wird, werden Änderungen am letzten Feld möglicherweise nicht beobachtet, da Verwendungen dieses endgültigen Felds zur Kompilierungszeit mit der Kompilierzeit Konstante ersetzt werden.

Aber, wenn Sie den Absatz über sehr sorgfältig zu lesen, Sie einen Weg, um hier finden (das private final Feld im Konstruktor statt in der Felddefinition):

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; 
    } 
    } 

} 

Der Ausgang ist dann wie folgt:

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

Hoffe das hilft ein bisschen.

+1

+1 - das ist die echte Antwort. Das Programm des OP verursacht die Annahme des JIT-Compilers, dass "s" sich nicht als ungültig ändert. Dies ist kein Compiler-Fehler, da die JLS explizit warnt, dass "schlimme Dinge" wie dies passieren können, wenn Sie 'final' Variablen ändern, nachdem sie gesetzt wurden. –

+5

@Stephen C: Eigentlich hat das Verhalten nichts mit * JIT * -Compiling zu tun. Es ist bereits der Bytecode-Compiler, der das 's' in' return "s =" + s; 'mit der Kompilierzeitkonstante ersetzt. Dies kann demonstriert werden, indem zwei Klassen gebildet werden, "A" und "B", so dass "A" sich auf eine Konstante bezieht, die in "B" definiert ist. Wenn Sie jetzt die Konstante in "B" ändern und nur "B" neu kompilieren, wird die alte Konstante in "A" gehalten! Hinterhältig, aber wahr. –

+0

@Joonas - ok ... aber es wäre auch legitim, dass der JIT-Compiler diese Art von Optimierung durchführt. –

1

Als final hat der Compiler erwartet, dass sich der Wert nicht ändert, daher hat er die Zeichenfolge wahrscheinlich direkt in Ihre -Methode codiert.

15

Diese

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

kompiliert tatsächlich wie folgt aus:

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

Das heißt, Kompilierung-Konstanten inlined erhalten. Siehe this Frage. Der einfachste Weg, inlining zu vermeiden, ist die String wie folgt zu erklären:

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

Für andere Arten, ein trivialer Methodenaufruf funktioniert den Trick:

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

Ich mag diese klare Erklärung und 'intern()' Ansatz, wusste nicht darüber vorher ... Danke. –

+3

Verwenden Sie eigentlich lieber 'toString', da eine Suche im String-Pool vermieden wird. –

6

Hier ist eine decompile von WithPrivateFinalField Klassendatei (Ich habe es in einer separaten Klasse der Einfachheit halber):

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 

Hinweis im toString() Methode, die konstant bei Adresse 0 [verwendet 0 ldc <String "s = I’m totally safe"> [23] ] zeigt an, dass der Compiler bereits das String-Literal "s = " und das private Endfeld " I’m totally safe" zusammen verkettet und gespeichert hat. Die Methode toString() gibt immer "s = I’m totally safe" zurück, unabhängig davon, wie sich die Instanzvariable s ändert.

Verwandte Themen