Über anonyme Funktionen in Java

von Hubert Schmid vom 2012-09-23

Die nächste Version von Java wird anonyme Funktionen – auch als Lambdas bekannt – unterstützen. Das ist ein sehr wichtiges und nützliches Feature. Ich betone das explizit, da ich mich zuletzt kritisch über Code mit anonymen Funktionen geäußert habe. Allerdings ging es dabei um eine übermäßige Verwendung auf eine ganz bestimmte Art. Setzt man sie hingegen richtig ein, führen sie zu einer signifikanten Vereinfachung – und zwar insbesondere in typischem Applikationscode.

Richtiger Einsatz bedeutet für mich, dass sie in erster Linie die Lesbarkeit und Wartbarkeit unterstützen und sich weitgehend natürlich anfühlen. Hier ein einfaches Beispiel:

List<Message> messages = ...; if (messages.anyMatch(m -> !m.isSeen())) { System.out.println("You have unseen messages."); }

Der Ausdruck m -> !m.isSeen() definiert die anonyme Prädikatsfunktion, die für den formalen Parameter m prüft, ob die Nachricht ungelesen ist. Der Parametertyp Message wird aus dem statischen Context bestimmt. Die Syntax ist angenehm und sehr ähnlich zu C# und anderen statisch typisierten Programmiersprachen, die sich als Alternative zu Java positionieren. Im Vergleich dazu eine Implementierung ohne anonyme Funktion:

List<Message> messages = ...; for (Message message : messages) { if (!message.isSeen()) { System.out.println("You have unseen messages."); break; } }

Der Unterschied in der Länge ist gering und nicht relevant. Wichtig ist hingegen die unterschiedliche Lesbarkeit. Die Variante mit anonymer Funktion sagt explizit was passiert: Wenn irgendeine Nachricht ungelesen ist, dann gib eine entsprechende Meldung aus. Die zweite Variante enthält hingegen eine ausformulierte Schleife, die man genau lesen muss, um sich diese Aussage zu erschließen.

Die Implementierung mit anonymer Funktion setzt die Unterstützung durch die Standardbibliothek voraus – im Beispiel in Form der anyMatch-Methode in java.util.List. In Zukunft wird es einige Methoden dieser Art geben – sozusagen einfache Algorithmen auf Collection-Instanzen. Hier ist ein weiteres, sehr ähnliches Beispiel:

int unseen = messages.count(m -> !m.isSeen()); if (unseen > 0) { System.out.println("You have " + unseen + " unseen message(s)."); }

Auch in diesem Fall ist klar wie der entsprechende Code mit externer Iteration aussehen würde, und dass die Kombination aus anonymer Funktion und Bibliotheksunterstützung eine Vereinfachung darstellt – zumindest sobald man ein wenig Erfahrung damit gesammelt hat. Vermutlich reichen so zwischen 10 und 20 Standard-Algorithmen aus um in typischem Applikationscode über die Hälfte aller Schleifen zu ersetzen. Hier noch ein Beispiel mit der filter-Methode:

List<Message> unseenMessages = messages .filter(m -> !m.isSeen()) .into(new ArrayList<>());

Und schließlich noch ein komplexeres Beispiel, in dem die Gesamtgröße aller Nachrichten bestimmt wird. Dafür wird zunächst mit der map-Methode zu jeder Nachricht die Größe bestimmt, die anschließend mit Aggregrationsfunktion reduce kumuliert werden:

int totalSize = messages .map(m -> m.getSize()) .reduce(0, Integer::add);

Doch Vorsicht: Dieser Ansatz skaliert nicht – bezüglich der Lesbarkeit. Das folgende Beispiel finde ich bereits grenzwertig. Statt nur die Summe über einen Nachrichtenordner zu berechnen, wird nun die Summe über alle Ordner berechnet.

int totalSize = folders .map(f -> f.getMessages() .map(m -> m.getSize()) .reduce(0, Integer::add)) .reduce(0, Integer::add);

Ich behaupte, dass die meisten Entwickler eine geeignete Variante mit expliziter Iteration schneller verstehen würden und anpassen könnten – selbst wenn sie mit anonymen Funktionen bereits erfahren sind. Eine alternative Möglichkeit besteht noch darin, den inneren Teil zu extrahieren und über eine lokale Variable zu referenzieren. Auch damit würde man vermutlich eine hinreichend gute Lesbarkeit erzielen.

Das letzte Beispiel soll zeigen, dass die Möglichkeiten der Komposition solcher Algorithmen relativ umfangreich sind, dass die einfache Lesbarkeit aber schnell nicht mehr gegeben ist. Der folgende Ausdruck bestimmt alle ungelesenen Nachrichten aus allen Ordnern, für die sich der Benutzer interessiert. Und aus meiner Sicht ist dieser Code bereits viel zu komplex, um in produktiven und geschäftskritischem Code aufzutauchen.

List<Message> unseenMessages = folders .filter(Folder::isSubscribed) .map(f -> f.getMessages().filter(m -> !m.isSeen()) .reduce(new ArrayList<>(), (lhs, rhs) -> { lhs.addAll(rhs); return lhs; }) .sort(comparing(Message::getDate)) .into(new ArrayList<>());

Die Beispiele sollten einen ersten Eindruck für die Vorteile anonymer Funktionen schaffen. Dabei ging es durchgehend um den Ersatz handgeschriebener Schleifen durch einfache Algorithmen. Die Beispiele habe ich ganz bewusst so gewählt, weil aus meiner Sicht an diesen Stellen die anonymen Funktionen ihre größte Verbreitung erreichen werden. Es gibt allerdings noch eine ganze Reihe von anderen interessanten Anwendungsfällen. Darauf werde ich in einem späteren Artikel eingehen.