Zur Laufzeit Werte von final-Felder ändern

Überraschende Effekte mit Java Reflection

Arno Haase

© Software & Suport Media

Wenn ein Feld einer Klasse final ist, muss man es im Konstruktor initialisieren. Danach behält es seinen Wert, solange das Objekt lebt. Oder doch nicht? Dieser Artikel betrachtet die Möglichkeiten, nachträglich die Werte von Feldern zu ändern, die als final deklariert sind. Man kann damit wilden Schabernack treiben. Es gibt aber auch sinnvolle Anwendungsbereiche dafür. Aber fangen wir mit der skurrilen Seite des Themas an.

Hallo – und Tschüs

Der Code in Listing 1 ruft zuerst eine Methode auf, die unbekannte Dinge tut. Anschließend gibt sie zur Begrüßung den Text „Hallo“ aus. Was wirklich auf der Konsole erscheint, hängt aber überraschenderweise davon ab, was die Methode magic() tut. Stringkonstanten sind in Java nicht ganz so konstant, wie man das landläufig erwartet.

Listing 2 zeigt eine Methode magic(), die aus der Stringkonstanten „Hallo“ den String „Tschüs“ macht – und zwar überall im Programm. Damit gibt der Code in Listing 1 „Tschüs“ aus, obwohl im Quelltext „Hallo“ steht.

magic();
System.out.println("Hallo");
static void magic() throws Exception {
  final Field field = String.class.getDeclaredField("value");
  field.setAccessible(true);
  field.set("Hallo", "Tschüs".toCharArray());
}

Das passiert per Reflection. Die Klasse String speichert ihre Zeichenkette in einem privaten char Array mit dem Namen value. Es gibt keine Garantie, dass das immer so bleibt, schließlich ist das Feld privat – aber in Java 1.7 ist String so implementiert.

Das Feld ist privat, und deshalb ruft der Code getDeclaredField auf der String-Klasse auf, weil diese Methode auch versteckte Felder liefert. Per Default erzwingt sie die Sichtbarkeitsregeln für Felder, auch wenn der Zugriff per Reflection erfolgt, verhindert also Zugriff auf private Felder.

Bevor unser Code das Feld verändern kann, muss er das ausschalten. Dazu ruft er setAccessible(true) auf. Nach diesen Vorbereitungen kann die Methode jetzt den Wert des Felds value ändern. Das tut sie auf dem String-Literal „Hallo“ und ersetzt dessen Inhalt durch die Zeichenfolge „Tschüs“. Das funktioniert, weil man in Java per Reflection auch final Felder verändern kann.

String.intern()

Aber warum verändert das den Wert von „Hallo“ an völlig anderen Stellen im Programm? Der Grund liegt darin, dass die JVM alle Stringkonstanten schon beim Laden der Klassen in einem globalen Pool ablegt, um Speicher zu sparen. Jedes String-Literal belegt dadurch nur einmal Speicher, egal in wie vielen Klassen es vorkommt.

Wenn also die magic()-Methode in Listing 2 den Wert des Strings „Hallo“ verändert, betrifft das gleichzeitig alle anderen Stellen in der JVM, die schon das gleiche String-Literal verwenden. Strings, die erst zur Laufzeit erstellt werden – z. B. mit einem StringBuilder – landen nicht automatisch in diesem globalen Pool.

Man kann aber mit der Methode String.intern() einen String ausdrücklich in den Pool schieben. Sie liefert einen String zurück, der dieselbe Zeichenkette enthält und garantiert im Stringpool liegt.

Listing 3 erzeugt mit einem StringBuilder eine Stringinstanz von „Hallo“, die nicht im Pool liegt, und erzeugt daraus durch Aufruf von intern() eine Kopie. Weil magic() auf dem globalen Pool operiert, behält die mit StringBuilder erzeugte Instanz ihren Wert. Die mit intern() erzeugte Kopie wird dagegen zu „Tschüs“.

final String s1 = new StringBuilder("Hallo").toString();
final String s2 = s1.intern();
assert(s1 != "Hallo");
assert(s2 == "Hallo");
magic();
System.out.println(s1); // "Hallo"
System.out.println(s2); // "Tschüs"

1 + 2 = 7

Der Stringpool ist nicht die einzige interne Datenstruktur der JVM, die man per Reflection verändern kann, um überraschende Effekte zu erzielen. So verwendet Java z. B. einen Cache zum Einpacken von int-Werten in Integer-Objekte (Auto-Boxing). Das ist eine wirksame Performanceoptimierung, die per Default für Werte von -128 bis 127 aktiv ist. Der Cache initialisiert ein Array mit allen zu cachenden Integer-Werten und bedient spätere Anfragen per Lookup aus diesem Array.

Dieses Array kann man per Reflection verändern (Listing 4). Es liegt in der privaten inneren Klasse Integer$IntegerCache in einer Variable cache. Diese Variable ist statisch, und deshalb übergibt der Code zum Auslesen den Parameter null an Field.get().

Der erste Eintrag im Array ist für den Wert -128, sodass cache[129] den Integer für die Zahl 1 enthält. Den ersetzt Listing 4 durch die Zahl 5. Anschließend liefert Auto-Boxing (und auch jeder Aufruf von Integer.valueOf) für die Zahl 1 den Wert 5. Der Ausdruck (Integer)1 + 2 ergibt also den Wert 7.

final Class cls = Class.forName("java.lang.Integer$IntegerCache");
final Field field = cls.getDeclaredField("cache");
field.setAccessible(true);
final Integer[] cache = (Integer[]) field.get(null);
cache[129] = 5;

System.out.println((Integer) 1 + 2); // 7

Sichtbarkeit

Nebenläufige Programme – und welches Programm ist das heute nicht – verlassen sich oft auf die Sichtbarkeits- und Konsistenzgarantien, die Java für final-Felder macht. Im Kern besagen diese Garantien, dass alle final-Felder fertig initialisierter Objekte in allen Threads vollständig sichtbar sind, ohne dass Konstrukte wie volatile oder synchronized nötig wären (für Details siehe Kapitel 17.5 der Java Language Specification JLS). Das ist ein so intuitives Programmiermodell, dass man es leicht für selbstverständlich nimmt. Aber was passiert, wenn man ein final-Feld per Reflection nachträglich ändert? Die JLS widmet diesem Fall interessanterweise ein eigenes Kapitel (17.5.3). Dort ist festgelegt, dass nach jeder Änderung eines final-Felds dieselben Sichtbarkeitsgarantien gelten, als ob das Objekt gerade erzeugt worden wäre.

Ein Thread, der ein final-Feld liest, darf dessen Wert aber ausdrücklich z. B. in einem Prozessorregister speichern und teilweise sogar inlinen, ohne je nachzuschauen, ob er sich im Speicher geändert hat. Wenn zwei Threads also dasselbe Objekt kennen, und einer von ihnen ändert ein final-Feld, gibt es keine Garantie, dass der andere Thread diese Änderung jemals sieht.

Fazit

Wozu soll es also gut sein, den Wert von final-Feldern per Reflection zu ändern? Ich hoffe es ist klar, dass die bösartigen Beispiele mit Strings und Integer-Cache nur die Mechanismen unterhaltsam demonstrieren, dass sie aber nicht als Anwendungsfall gedacht sind.

Tatsächlich kenne ich nur einen einzigen sinnvollen Grund, so etwas zu tun: Deserialisierung von Objekten. Generischer Code, der Objekte aus einer Bytefolge rekonstruiert, kann im Allgemeinen nicht wissen, welchen Konstruktor er mit welchen Werten aufrufen muss, um einen bestimmten Objektzustand wiederherzustellen.

Aber er kann ein „leeres“ Objekt erzeugen und mit den hier beschriebenen Mechanismen die Felder per Reflection initialisieren. Und wenn er das Objekt erst nach abgeschlossener Initialisierung für andere sichtbar macht, garantiert die JLS die gleichen intuitiven Sichtbarkeitsregeln, als ob das Objekt vollständig in einem Konstruktor befüllt worden wäre.

Im letzten Teil der Kolumne ging es übrigens um Die Tücken bei der Performancemessung – zu finden hier auf JAXenter.

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: