Was Entwickler mit Java 8 erwartet

Seite 2: Kompatibilität und Referenzierung

Inhaltsverzeichnis

Lambda-Ausdrücke helfen zwar bei der Implementierung anonymer Klassen, sie sind aber mehr als eine Kurzschreibweise hierfür. Für herkömmliche anonyme Klassen gelten nämlich andere Gültigkeitsebenen als für Lambda-Ausdrücke: Der Lambda-Ausdruck kann direkt auf Variablen zugreifen, die in derselben Codeebene zugänglich sind, in der er definiert wird. Der entsprechende Mechanismus wird als Variable Capture bezeichnet.

Dabei gibt es allerdings eine wesentliche Einschränkung: Lokale Variablen müssen final deklariert sein oder sich zumindest so verhalten (effectively final); das bedeutet, dass die Variablen nach ihrer ersten Zuweisung nicht mehr verändert werden und der Java Compiler eine Deklaration mit final ohne Fehlermeldung akzeptieren würde.

// Lambda-Ausdrücke können auf finale Variablen zugreifen
final String pattern = ".pdf";
FilenameFilter pf =
(File f, String s) -> s.toLowerCase().endsWith(pattern);

Auch bei this und super verhält sich Code in Lambda-Ausdrücken anders als Code in anonymen Klassen. Normalerweise bezieht sich this auf die Instanz der Klasse und super auf die Instanz der übergeordneten Klasse. Definert man also auf herkömmliche Weise eine anonyme Klasse, verweist this auf Elemente innerhalb dieser.

In Lambda-Ausdrücken haben this und super dagegen dieselbe Bedeutung wie im Code außerhalb: this bezieht sich auf Elemente der Klasse, in der der Lambda-Ausdruck definiert wird, super auf deren Basisklasse.

Es gibt unzählige Schnittstellen in der Java-Standardbibliothek, die prädestiniert zur Anwendung von Lambda-Ausdrücken sind. Das würde allerdings Änderungen an den Schnittstellen erfordern und so die Kompatibilität zu Millionen von Java-Programmen auf's Spiel setzen.

Ein Beispiel ist die Schnittstelle Iterable: Sie wurde in Java 8 um die neue Methode forEach erweitert, die einen Lambda-Ausdruck als Parameter erwartet. Eigentlich wäre jetzt der Code jeder Klasse, die von Iterable abgeleitet ist, um die Implementierung von Iterable zu erweitern. Es gäbe praktisch kein Programm, das durch das Update auf Java 8 und die neue Java-Standardbibliothek nicht grundlegend zu ändern wäre.

Der Ausweg aus diesem Dilemma sind Default-Methoden: Beginnend mit Java 8 können bei der Definition von Schnittstellen einzelne Methoden mit dem Schlüsselwort default deklariert und mit Code versehen werden. Bei der Implementierung der Schnittstelle hat der Programmierer die Wahl, entweder die Implementierung der Default-Methoden zu übernehmen oder die Methode durch eigenen Code zu überschreiben.

/// Default-Methoden in Schnittstellen
interface Iterable<T> ... {
...
void forEach(Consumer<? super T> action) default {
Iterables.forEach(this, action);
}
}

Der obige Codeausschnitt zeigt die Deklaration der neuen Methode forEach für die Iterable-Schnittstelle. Dank des Schlüsselworts default ändert sich für vorhandenen Java-6- oder Java-7-Code nichts: Sämtliche Klassen, die Iterable implementieren, funktionieren weiterhin. Aber allen Java-8-Programmierern steht nun die neue Methode forEach zur Verfügung. Sie lässt sich wahlweise in der Default-Implementierung nutzen oder durch eine eigene, für den besonderen Anwendungsfall vielleicht effizientere Variante ersetzen.

Die Schreibweise Klasse::statischeMethode übergibt eine Referenz auf eine statische Methode. Im folgenden Beispiel wird an sort die statische compare-Methode der Double-Klasse übermittelt. Alternativ lässt sich die Vergleichsmethode, also eine anonyme Comparator-Klasse, auch als Lambda-Ausdruck formulieren. (Selbstverständlich können Double-Arrays wie bisher auch ohne einen Comparator sortiert werden, weil die Double-Klasse ja ohnedies die Schnittstelle comparable implementiert; die Beispiele sollen nur die neuen Syntaxmöglichkeiten verdeutlichen.)

//  sort-Aufruf mit Methodenreferenzen
import java.util.*;
Double[] x = {1.23, 1.2, 1.29};
Arrays.sort(x, Double::compare);

// sort-Aufruf mit Lambda-Ausdruck
Arrays.sort(x,
(d1, d2) -> d1==d2 ? 0 : (d1<d2 ? -1: 1));

Auch die nicht statischen Methoden von Objekten (also von konkreten Instanzen einer Klasse) lassen sich als Parameter übergeben. Die Schreibweise lautet objektvariable::instanzMethode. In der Praxis ist diese Syntax aber selten hilfreich, weil damit zwar eine Referenz auf die Methode übergeben wird, nicht aber das zu bearbeitende Objekt.

Java unterstützt deswegen eine zweite Syntaxvariante, die formal gleich wie bei statischen Methoden aussieht: klasse::instanzMethode. Tatsächlich wird dadurch aber ein Lambda-Ausdruck der folgenden Form gebildet:

(Klasse obj) -> { obj.instanzMethode(); }

Zweckmäßig ist diese Schreibweise zum Beispiel in Kombination mit der neuen forEach-Methode diverser Aufzählungsklassen. Sie können nun an forEach eine Methode übergeben, die dann auf alle Objekte der Aufzählung angewandt wird:

lst.forEach(MyClass::printout);