Valhalla & Feature Flags in Java
Ich muss zugeben, in der letzten Zeit habe ich mich mal wieder ungesund viel mit spekulativem Kram beschäftigt, der wahrscheinlich, und nur vielleicht, in ein paar Jahren relevant wird.
Letzte Woche rutschte einer der Talks vom JVMLS (Java Virtual Machine Language Summit) 2024 in mein YouTube-Autoplay, und im Affekt habe ich mir dann irgendwie alle angeschaut.
JVM Language Summit
Der JVMLS ist eine jährliche Convention bei der unter anderem über anstehende Neuerungen und verschiedene, laufende Projekte der Java Platform von den jeweiligen Experten berichtet wird. Es gab dieses Jahr viele interessante, und empfehlenswerte Präsentationen, zum Beispiel:
- 🎞 von Paul Sandoz zu Project Babylon über tiefere Reflection, mit der man auch in entsprechend annotierte Methoden hereingucken können soll, (leider noch sehr in den Kinderstiefeln)
- 🎞 von Lorris Créantor und Rémi Forax zur teilweisen Reification von Generics, (sehr interessant, wenn auch, durch die starken französischen Akzente nur bedingt verständlich)
- 🎞 von Ioi Lam und Dan Heidinga zu Project Leyden, in dem die Leute der HotSpot-Group an-leyden dass die JVM beim Startup nicht so leyden muss (Ich hör ja schon auf :D)
und: was ich am interessantesten fand:
- 🎞 von Brian Goetz, dem obersten Java-Gott, zu Project Valhalla, dem knackigen Wikingerspektakel für die ganze Familie.
Project Valhalla
Der Himmel der nordischen Gottheiten wird in Mythen ja immens cool dargestellt, und da musste das entsprechende Java-Projekt natürlich auch nachziehen. Ambitioniert ging es bereits 2014 los, und unter dem nicht ganz subtilen Subtitel “Javas Epic Refactor”, wurden mit der Zeit einige große Featurevorschläge vereint.
Hier ein kurzer Überblick über das Wesentliche:
In vielen Java-Projekten gibt es Klassen, die Werte, oder auf Werten basierende Datentypen abbilden. Beispiele aus der Standardbibliothek sind String
, Instant
und Optional
, Beispiele aus jeder Business-Application Email
, Euro
oder irgendeine HttpJsonResponse
.
Beispiele für Klassen, die keine wertebasierten Typen repräsentieren, sind z.B. alle, die eine Art State verkörpern, also in sich veränderbar sind. Hierzu gehören Klassen wie ArrayList
, Thread
und Random
sowie auch alle Implementierungen von Closeable
und ExecutorService
.
In Valhalla wird bei den erstgenannten Klassen zwischen value-types und value-based types unterschieden; was das bedeutet, ist erstmal egal; das Hauptfeature ist fehlende Identität.
Identität
Erinnerst du dich noch daran, als du Java gelernt und das erste Mal zwei Strings mit ==
verglichen hast? Damals hat dir dein Mentor wahrscheinlich auf die Finger gehauen und den Unterschied zwischen ==
und .equals()
erklärt. Genau hier kommt Identität ins Spiel.
Zwei Strings können denselben Text und dieselbe Identität haben. Müssen sie aber nicht.
String a = "Hallo";
String b = "Hallo"; // a == b (gleiche Identität)
String c = new String(b); // c != a und c != b (ungleiche Identitäten)
String d = c; // d == c und damit auch d != a und d != b
Die Erklärung dafür warum das so ist, hat unter anderem mit dem Constant-Pool zu tun und wäre jetzt wirklich ein großer Umweg, letztendlich zählt jedoch nur, dass in der Realität eigentlich alle 4 Strings gleich sind, da sie alle den Text, bzw. Wert "Hallo"
repräsentieren, und das, obwohl es zwei verschiedene Instanzen mit unterschiedlicher Identität sind.
Jetzt sind wir wieder bei value-types, allen Datentypen, die man auf keinen Fall mit ==
vergleichen will, da deren Wert immer interessanter ist, als deren Identität.
Project Valhalla nimmt sich mit JEP 401 zum Ziel, Klassen mit dem neuen Keyword value
um ihre Identität zu erleichtern. (Die Motivation ist verbesserte Performance)
Restricted Nullability
Wer Javas kleinen Sohn Kotlin kennt und liebt, tut dies vermutlich unter anderem wegen der optionalen “nullness”. In Kotlin muss man für jeden Datentyp explizit erlauben, dass er null
sein darf; von alleine kann er das nämlich nicht.
String a = null; // ⛔️ Möp! Compilerfehler!
String? b = null; // ✅️ Erlaubt! (Man beachte das "?", welches dem String einen bewusst unsicheren Unterton verleiht)
Java will das jetzt auch können, hat nur leider dieses nervige Problem der backwarts compatibility: Jeder Datentyp ohne Zeichen erlaubt bisher null
. Das jetzt zu ändern würde fast alle existierenden Javaprogramme in Compilerfehler stürzen, zugegeben, welche, die mit einfachsten Refactorings zu beheben wären, jedoch ist das halt nicht der Stil der Sprache.
Stattdessen wird der null-restricting-operator !
vorgestellt, mit dem man jetzt auch verbieten kann, dass ein Typ null
ist. Sinnvoll, jaja, jedoch soll er ebenfalls von einem ?
begleitet werden. Damit gibt es dann die folgenden Möglichkeiten:
String! a = null; //❗️Möp! Compilerwarnung. Nicht Fehler, es könnte ja passieren, dass die null aus einem legacy-System kommt
String? b = null; // ✅️ Erlaubt! Das "?" sagt wie in Kotlin explizit, dass dieser String null sein kann.
String c = null; // ✅️ Auch erlaubt! Das alte System ohne Operator bekommt jetzt die Semantik: "Vielleicht null? Keine Ahnung 🤷🏼♂️"
Der praktische Unterschied zwischen keinem Operator und ?
ist hier schwer erkennbar (wahrscheinlich unsichtbare Compileroptimierung), denn am Ende des Tages ist egal, ob ein Feld “explizit-vielleicht” null
ist, oder man das Gegenteil einfach nicht nachweisen kann.
Na ja, schön, dass es immerhin so viele Optionen gibt, dann schreib’ ich halt in Zukunft immer ein !
hinten ran.
Strict Initialization
Hast du schonmal final
ausgetrickst? Geht ganz einfach, ich zeigs dir:
class SubCls extends SuperCls {
final String foo; // Das sollte sich nicht ändern, oder?
SubCls(String foo) {
super(this);
this.foo = foo;
System.out.println(this.foo); // > "Yeyah!", wie erwartet
}
}
class SuperCls {
SuperCls(SubCls sub) {
System.out.println(sub.foo); // > "null" Wat is denn hier passiert?
}
}
var instance = new SubCls("Yeyah!");
System.out.println(instance.foo); // Wieder > "Yeyah!", wie erwartet
Mhh.. bisschen unintuitiv, aber mit den Vorschlägen vom JVMLS, mit den existierenden Previews aus JEP 482 könnte dieses Problem bald der Vergangenheit angehören.
Das Problem hier ist, dass im super
-call die nicht ganz initialisierte Instanz “escaped” und die JVM dieser ja irgendeinen Standardwert geben muss. Mit der Neuinterpretation des ACC_STRICT
-Flags durch Project Valhalla, kann dies jedoch verboten werden, und man wird zum Beispiel in value-classes dazu gezwungen, noch vor dem super
-call zu initialisieren.
Mein Problem mit dem Ganzen
Halloo! Stop! Erstmal das Positive.
Ich finde es persönlich sehr gut, dass es diese und andere Features bald geben soll, da sie sowohl die Performance als auch die Lesbarkeit verbessern.
Mein Problem ist die Art des Opt-Ins in diese Features und die fehlende Möglichkeit, die richtigen Entscheidungen als default zu treffen. Brian Goetz stimmt hier übrigens zu, traut sich aber leider nicht an radikalere Maßnahmen:
Investigations into null-excluding type systems have shown that the better default would be to treat an unannotated name as indicating non-nullability, and use explicitly nullable types (
T?
) to indicate the presence of null, because returning or accepting null is generally a less common case. Of course, todayString
means “possibly nullable String” in Java, meaning that, yet again, we seem to have chosen the wrong default.
Zitat: valhalla-spec-observers
Was mein ich damit?
Klassen und vor allem Records halten aus meiner Sicht inhärent Werte, und über deren Identität verliert man sowieso schnell den Überblick. Ich kann mich nicht daran erinnern, bei einem von beiden absichtlich ==
außerhalb einer equals
-Implementation verwendet zu haben.
Value based ist also der Default; Identität die Ausnahme!
Kotlin macht es richtig. Ich möchte nur seltenst, dass Felder null
sein dürfen, und dann möchte ich dies explizit kennzeichnen, da “nullable” der gefährlichere Typ ist, weil er zur Laufzeit zu Exceptions führen kann.
Null-Restricted ist also der Default; Nullable die Ausnahme!
Wenn ich ein Feld final
mache, erwarte ich, dass es seinen Wert nicht ändert. Während der Initialisierung von Objekten ist es jedoch aktuell absolut möglich, dass das Ding auch kurz null
sein kann, obwohl es später im Konstruktor gesetzt wird. Weder schön noch sicher.
Strict initialization ist also der Default; Lazyness die Ausnahme!
Fazit
Java rühmt sich mit dem Versprechen, dass fast jeder Code in jeder späteren Version noch läuft, jedoch sehe ich hier ein großes “Leider” für viele neue Features und Refactorings. Ich fände es persönlich viel ansprechender, wenn man für brandneue Projekte den Compiler und die VM mit entsprechenden Feature-Flags starten könnte, welche die gewünschten Defaults standardmäßig aktivieren.
--value-based-by-default --null-restricted-by-default --strict-init-by-default
Hier müsste nur noch die Brücke zu veralteten Libraries gebaut werden, jedoch könnte man sogar binary-compatibility dadurch schaffen, dass man, mit dem neuen Flag getaggte Versionen implizit konvertiert und alte Versionen mit den durch Valhalla vorgeschlagenen spezifischen opt-ins anpasst.
Ein Beispiel
Neuer Code mit --null-restricted-by-default
String a = null; // Compilerfehler durch Flag, a ist null-restricted
String! b = null; // Überflüssiges "!", vielleicht eine Info zur Redundanz
String? c = null; // Kompiliert, da c explizit null erlaubt
Legacy Version sieht diesen als:
// Kompiliert, denn bisher gibt es keine null restriction, nur raw types
String a = null;
String b = null;
String c = null;
Neuer Code ohne --null-restricted-by-default
sieht diesen als:
String! a = null; // Kompiliert, "!" jetzt notwendigerweise explizit
String! b = null; // Kompiliert, "!" wird übernommen
String? c = null; // Kompiliert, "?" wird übernommen
Über die nächsten Jahre wäre so eine Migration in eine bessere Welt möglich; es könnte nur etwas mehr Zeit in Anspruch nehmen. Schlussendlich können die Flags dann abgeschafft und als wahrer Standard übernommen werden.
In meinen Augen würde man erst damit tatsächlich Valhalla betreten.