Unveränderlichkeit ist ein wichtiges Programmierparadigma. Es verbessert die Lesbarkeit und Wartbarkeit, schützt vor Programmierfehlern und erhöht die Sicherheit. Eingeschleuster Schadcode kann unveränderliche Variablen nicht manipulieren.
In Java wird hierfür (u.a.) das Keyword final benutzt. Eine finale Variable kann man nach ihrer Instanziierung nicht mehr ändern. So war es zumindest zum Start von Java 1995. Doch 2004 wurde das mit Java 5 aufgeweicht. Neue Frameworks wie Spring und Hibernate verlangten mehr Flexibilität. So konnte man nun per Reflection den Wert von finalen Instanz- und Klassenvariablen ändern. Final war damit nur noch eine Absichtserklärung, keine Garantie mehr. Es dauerte nicht lange, da erkannte man das Desaster. Es war jetzt sogar möglich, Boolean.TRUE global auf FALSE zu setzen:
import java.lang.reflect.Field;
// Läuft mit Java 8
public class TrueIsFalseAndFalseIsTrue {
public static void main(String[] args) throws Exception {
// Hole das private, finale Feld "value" aus der Klasse Boolean
Field valueField = Boolean.class.getDeclaredField("value");
// Umgehe die Sichtbarkeitsprüfung
valueField.setAccessible(true);
// Verhexe TRUE und FALSE
valueField.setBoolean(Boolean.TRUE, false);
valueField.setBoolean(Boolean.FALSE, true);
// Und das Unheil ...
if (!Boolean.TRUE && Boolean.FALSE) {
System.out.println("Fair is foul and foul is fair.");
}
// ... nimmt seinen Lauf.
}
}
Seitdem versuchen Java-Sprachentwickler diesen Design-Fehler peu à peu zu fixen. Mit Java 16 (2021) ist der reflexive Zugriff über Modulgrenzen hinweg im Default verboten. Damit kann man interne Java-Klassen nicht mehr von außen modifizieren. Der obige Boolean-Hack bricht mit einer InaccessibleObjectException ab. Ebenfalls mit Java 16 wurde das reflexive Verändern von finalen Klassenvariablen verboten. Diese sind seitdem wieder unveränderlich.
Mit finalen Instanzvariablen tat sich die Java-Community etwas schwerer. Hier waren es immer noch die großen Frameworks wie Spring und Hibernate, die das ausnutzen. Sie müssen entsprechend angepasst werden – Stichwort Konstruktor-Injection. Doch es gibt Licht am Ende des Tunnels. Mit JEP 500: Prepare to Make Final Mean Final (Java 26) wird nun eine Warnung ausgegeben, wenn finale Instanzvariablen geändert werden. Und mit Java 28 oder 29 (2027) soll es im Default komplett verboten werden. Das kann man mit --illegal-final-field-mutation=deny auch schon heute scharf schalten. Dann ist final wieder final.
import java.lang.reflect.Field;
class FinalPerson {
private final String name;
public FinalPerson(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
void main() throws Exception {
var person = new FinalPerson("Alice");
IO.println("I am " + person.getName());
// Change final name using reflection
var field = FinalPerson.class.getDeclaredField("name");
field.setAccessible(true);
field.set(person, "Cheshire Cat");
IO.println("I am " + person.getName());
}
// Ausgabe Java 25:
// I am Alice
// I am Cheshire Cat
// Ausgabe Java 26:
// I am Alice
// WARNING: Final field name ... has been mutated ...
// I am Cheshire Cat
// Ausgabe Java 26 mit --illegal-final-field-mutation=deny
// Oder zukünftig ab Java 28/29:
// I am Alice
// ... IllegalAccessException ...
// ... not allowed to mutate final fields ...
Und final sollte man – wo immer möglich – nutzen. Finale Variablen (und auch finale Klassen) führen, wie oben angerissen, zu sauberem und sichererem Code. Zusätzlich können sie auch die Performance verbessern. Ein Compiler kann nämlich unveränderliche Variablen durch ihre konstanten Werte ersetzen. Die Technik dahinter heißt Constant Folding und Constant Inlining. In einfachen Fällen macht das der Java-Compiler schon immer. Doch komplexes Constant Inlining bzw. Folding, ausgeführt vom JIT-Compiler zur Laufzeit, ist nur möglich, wenn Konstanten wirklich konstant sind. Und das wird in Zukunft wieder der Fall sein.
Weitere Details findet man im ausgezeichneten Artikel von Sven Woltmann.