Typinferenz fĂĽr lokale Variablen in Java 10, Teil 2

Seite 2: Ad-hoc-Methoden

Inhaltsverzeichnis

Ähnlich wie Felder lassen sich auch Methoden hinzufügen:

var corp = new Megacorp(/* ... */) {
final BigDecimal SUCCESS_BOUNDARY = new BigDecimal("500000000");

boolean isSuccessful() {
return earnings().compareTo(SUCCESS_BOUNDARY) > 0;
}

boolean isEvil() {
return true;
}
};

Wäre corp als Megacorp deklariert, stünden die neuen Methoden isSuccessful und isEvil nicht zur Verfügung. Mit var schon:

var corp = // like before
System.out.printf(
"Corporation %s is %s and %s.\n",
corp.name(),
corp.isSuccessful() ? "successful" : "a failure",
corp.isEvil() ? "evil" : "a failure"
);

Das Prinzip ist identisch mit den Ad-hoc-Feldern. Die Kritik ebenfalls: Lesbarkeit und Veränderbarkeit des Codes leiden ohne nennenswerten Gegenwert. In diesem Fall könnten Methoden wie isSuccessful oder isEvil einfach direkt Teil der Klasse oder einer Unterklasse sein oder, wenn das nicht möglich oder wünschenswert ist, als statische Helfermethoden implementiert werden. Das würde es auch ermöglichen, sie an anderer Stelle einfach wiederzuverwenden.

Gelegentlich ist man in der Situation, dass ein Methodenparameter zwei Interfaces implementieren soll:

static <E> Optional<E> firstMatch(
/* something which is Iterable<E> and Closeable */ elements,
Predicate<? super E> condition)
throws IOException {
try (elements) {
return stream(elements)
.filter(condition)
.findAny();
}
}

Hier soll über elements iteriert werden (deswegen Iterable) und die Quelle nachher über den Try-with-resources-Block geschlossen werden (deswegen Closeable). Der Parameter elements soll also sowohl den Typ Iterable als auch Closeable haben oder, in anderen Worten, der Typ von elements ist genau die Überschneidung der Typen Iterable und Closeable – daher der Ausdruck Intersection Type.

Venn-Diagramm zum Konzept Intersection Types

Wie kann man das in Java ausdrücken? Der übliche Ansatz ist, ein neues Interface einzuführen, dass die benötigten erweitert:

public interface CloseableIterator<E> extends Closeable, Iterator<E> { }

static <E> Optional<E> firstMatch(
CloseableIterator<E> elements,
Predicate<? super E> condition)
throws IOException {
// ...
}

Das Problem dabei ist, dass existierende Klassen aus Bibliotheken, Frameworks oder dem JDK nichts von diesem Interface wissen und man sie deswegen nicht mit der neuen Methode verwenden kann – selbst wenn sie die eigentlich benötigten Interfaces implementieren:

// 'Scanner' implements 'Iterator<String>' and 'Closeable'
Scanner scanner = new new Scanner(System.in);
Optional<String> dollarLine =
// compile error because 'scanner' is no 'CloseableIterator'
firstMatch(iterator, s -> s.startsWith("$"));

Das ist ärgerlich und es gibt in Java keine naheliegende Lösung dafür. In manch anderen Sprachen kann man Intersection Types direkt deklarieren. In Java könnte das vielleicht so aussehen, dass man Iterable<E> & Closeable elements anstelle von CloseableIterator<E> schreibt. Ganz so einfach ist es nicht, aber Generics bilden die Brücke dahin:

private static <E, T extends Iterator<E> & Closeable> Optional<E>
firstMatch(T elements, Predicate<? super E> condition)
throws IOException {
// ...
}

Wie gesagt, nicht naheliegend, aber es funktioniert. Hier kommen sogenannte Bounded-Type-Parameter zum Einsatz, um auszudrĂĽcken, dass elements vom Typ T sein soll, der wiederum sowohl Iterator<E> als auch Closeable implementieren muss. T ist also der Schnitt, die Intersection, aus Iterator<E> und Closeable.

So weit, so gut – das geht alles schon lange vor Java 10. Wo bleibt var? Wie zuvor auch kommt var ins Spiel, wenn eine Variable zu deklarieren ist. Analog zu firstMatch kann man folgende Factory-Methode schreiben:

static <T extends Iterator<String> & Closeable> T openCloseableIterator() {
return (T) new Scanner(System.in);
}

Das Problem dabei ist, dass die naheliegende Instruktionskette "rufe openCloseableIterator auf, weise das Ergebnis einer Variable zu und reiche die an firstMatch weiter" so nicht trivial aufzuschreiben ist. Deklariert man das Ergebnis von openCloseableIterator entweder als Iterator<String> oder als Closeable, beschwert sich der Compiler beim Aufruf von firstMatch, dass das jeweils andere Interface nicht implementiert ist. Wie bei Parametern ist die direkte Deklaration von Iterator<String> & Closeable in Java nicht erlaubt. Allerdings funktioniert der gleiche Trick mit Bounded-Type-Parametern:

static <T extends Iterator<String> & Closeable> void readAndPrint()
throws IOException {
T iterator = openCloseableIterator();
Optional<String> dollarLine =
firstMatch(iterator, s -> s.startsWith("$"));
System.out.println(dollarLine);
}

Man muss allerdings sagen, dass der Spaß mit Generics nun langsam aufhört. In openCloseableIterator und firstMatch ergibt die öffentlich sichtbare, generische Deklaration von T Sinn, denn sie schränkt einen Rückgabe- beziehungsweise Parametertyp ein. Aber bei readAndPrint taucht T in der Signatur gar nicht mehr auf. Das ist verwirrend für den Aufrufer. Ganz zu schweigen davon, was man machen muss, wenn es eine zweite solche Variable braucht.

Und hier kommt endlich var ins Spiel: Wie schon zuvor kann man damit leicht Variablen deklarieren, die einen Typ haben, der in der Java-Syntax nicht ausdrĂĽckbar ist:

static <T extends Iterator<String> & Closeable> void readAndPrint()
throws IOException {
var iterator = openCloseableIterator();
Optional<String> dollarLine =
firstMatch(iterator, s -> s.startsWith("$"));
System.out.println(dollarLine);
}

Die gesamte Konstruktion von Intersection Types ist ähnlich wie Ad-hoc-Felder und -Methoden nicht trivial und lebt vom Zusammenspiel komplexer Java-Features, hier Bounded-Type-Parameter und Typinferenz. Im Gegensatz zu den anderen beiden Anwendungen gibt es hier jedoch manchmal einfach keine Alternative. Darüber hinaus sind Intersection Types ein Konzept, das in vielen Programmiersprachen von Bedeutung ist, sodass sich damit auseinanderzusetzen ein echter Mehrwert für jeden Entwickler ist.

Wer also zu gelegentlich zu Intersection Types greifen muss, wird sich freuen, dass var die Verwendung an manchen Stellen deutlich vereinfachen kann. In solchen Situationen kann man es dann auch ohne schlechtes Gewissen einsetzen.