Typinferenz für lokale Variablen in Java 10, Teil 2

Für 'var' leitet der Compiler den Typ ab. Der kennt aber ein reichhaltigeres Typsystem, als die Sprache erlaubt, wodurch 'var' einige Tricks ermöglicht.

In Pocket speichern vorlesen Druckansicht 15 Kommentare lesen
Typinferenz für lokale Variablen in Java 10, Teil 2

(Bild: Pixabay)

Lesezeit: 16 Min.
Von
  • Nicolai Parlog
Inhaltsverzeichnis

Typinferenz mit var öffnet die Tür zu ein paar interessanten Tricks, bei denen Entwickler Variablen deklarieren können, obwohl sie keinen Typ haben, den man explizit ausdrücken könnte. Das heißt, obwohl Compiler und JVM einen konkreten Typ prinzipiell korrekt verarbeiten können, erlaubt die Syntax von Java nicht, ihn in einer Deklaration zu verwenden – mit var kann der Compiler ihn aber ableiten und korrekt in den Bytecode schreiben. Das können Entwickler zu verschiedenen Zwecken verwenden:

  • Erstellen von ad-hoc-Feldern
  • Erstellen von ad-hoc-Methoden
  • Bessere Verwendung von Intersection Types
  • Nachbildung von Mixins

Wenn man einen Konstruktor aufruft, um eine neue Instanz zu erstellen, hat man die Chance, ein paar Felder oder Methoden hinzuzufügen, indem man zwischen Parametern und Semikolon geschweifte Klammern und ein wenig Code einfügt:

User user = // ...
Map<User, SimpleAddress> addresses = // ...
Object userWithAddress = new Object() {
User _user = user;
Optional<SimpleAddress> _address =
Optional.ofNullable(addresses.get(user));
};
// does not compile
System.out.println(userWithAddress._user);

Dabei erstellt der Compiler ad hoc eine anonyme Unterklasse (hier von Object) und instanziiert sie. Allerdings kann der Quellcode diese Klasse nicht referenzieren – sie ist ja namenlos – und man kann deswegen keine Variable mit diesem Typ deklarieren. Die Folge ist, dass man userWithAddress als Object deklarieren muss und userWithAddress._user nicht kompiliert – Object hat kein Feld _user.

Anders sieht die Sache mit var aus:

User user = // ...
Map<User, SimpleAddress> addresses = // ...
var userWithAddress = new Object() {
User _user = user;
Optional<SimpleAddress> _address =
Optional.ofNullable(addresses.get(user));
};
// compiles
System.out.println(userWithAddress._user);

Auf der rechten Seite der Zuweisung passiert beim Kompilieren dasselbe wie zuvor. Links allerdings kann der Compiler jetzt den Typ eintragen, den er soeben erstellt hat. Da dieser ein Feld namens _user hat, kompiliert auch der Zugriff userWithAddress._user.

Das obige Beispiel wirkt etwas gewollt – warum sollten Entwickler userWithAddress so deklarieren wollen? Ein möglicher Anwendungsfall findet sich in längeren Aufrufketten, zum Beispiel Stream Pipelines. Da ist die Situation häufig, neben dem eigentlichen Element vorübergehend ein anderes Datum zu benötigen. Aber mangels Tupeln ist das nicht einfach auszudrücken. Da können Ad-hoc-Felder ganz gelegen kommen:

List<User> users = // ...
Map<User, SimpleAddress> addresses = // ...
Optional<User> user = users.stream()
.map(user -> new Object() {
User _user = user;
Optional<SimpleAddress> _address =
Optional.ofNullable(addresses.get(user));
})
.filter(o -> o._address.isPresent())
.filter(o -> isValid(o._address.get()))
.map(o -> o._user)
.findAny();

Spannenderweise benötigt das Beispiel nicht mal var, weil der anonyme Typ nur innerhalb der Stream Pipeline existiert. Folgerichtig kompiliert und läuft das Beispiel auch auf Java 8. Wo var hilft, ist bei der Deklaration von Zwischenvariablen, zum Beispiel wenn man die Adresse gerne behalten möchte:

List<User> users = // ...
Map<User, SimpleAddress> addresses = // ...
var user = users.stream()
.map(user -> new Object() {
User _user = user;
Optional<SimpleAddress> _address =
Optional.ofNullable(addresses.get(user));
})
.filter(o -> o._address.isPresent())
.filter(o -> isValid(o._address.get()))
.findAny();

Ohne das zweite map ist user ein Optional<anonymous class with two fields>, was sich ohne var nicht ausdrücken ließe.

Immer, wenn man neue Tricks entdeckt, sollte man sich allerdings fragen, ob man sie auch einsetzen sollte. In diesem Fall rät der Autor davon ab. Es gibt bestimmt Situationen, in denen es keine bessere Lösung gibt, aber man sollte sie nicht als normales Werkzeug am Entwicklergürtel betrachten.

Zum einen ist das Erstellen einer anonymen Klasse relativ umfangreich, da ihre Felder an Ort und Stelle zu deklarieren sind. Zum anderen ist die Mischung von anonymen Klassen und Typinferenz, beides nicht gerade Einsteigerthemen, nicht trivial und reduziert die Verständlichkeit des Codes.

Am meisten stört aber, dass das Konstrukt einigen einfachen Refactorings nicht standhält. Wenn im vorherigen Beispiel user in einem halben Dutzend Zeilen weiterverarbeitet wird, könnten aufmerksame Kolleginnen auf die Idee kommen, die Stream Pipeline und die Weiterverarbeitung in zwei Methoden zu trennen, um den Code lesbarer zu machen. Prinzipiell die richtige Idee, aber der var-Trick verhindert das sonst mechanische Refactoring, weil das Ergebnis der Pipeline keinen Typ hat, den man für den Rückgabewert der neuen Methode gebrauchen könnte:

List<User> users = // ...
Map<User, SimpleAddress> addresses = // ...
// 'determineUser' must be declared to return a concrete
// type, so no amount of 'var' magic is gonna help here
/*...*/ user = determineUser(users, addresses);
processUser(user);

Um das Refactoring durchzuziehen, wäre also eine Menge Code zu ändern, was die Wahrscheinlichkeit senkt, dass es jemand tatsächlich durchführt. Der Erfahrung nach sollten Aufräumarbeiten nicht schwerer als unbedingt nötig sein. Vor dem Hintergrund ist dieser Ansatz also zu vermeiden.

Alternativ können Entwickler für Paare Map.Entry verwenden, den man seit Java 9 mit der lesbaren, statischen Methode Map::entry erstellen kann. Wem das nicht genügt, sollte sich nach einer Bibliotheken umschauen, die Tupel mitbringt oder auf eine zukünftige Java-Version warten, in der es Record und/oder Value Types gibt.