java8 lambda

Eine der wichtigsten und lang ersehnten Neuerungen von Java 8 sind die Lambda Expressions. Lange musste die Java-Gemeinde auf diese Änderung im Java Development Kit (JDK) warten, doch mit Java 8 ist es endlich soweit. In diesem zweiten Teil unserer Artikelreihe stellen wir Ihnen die Funktionsweise von Lambda Expressions und Method References vor und überprüfen, ob das Versprechen von kürzerem und besser verständlichem Java Code erfüllt wird.

 

Historie

Lambda Expressions oder auch anonyme Funktionen ermöglichen es einer Methode, eine Funktion als Parameter zu übergeben. Lambda Expressions sind bereits seit 1958 in der Programmiersprache Lisp verfügbar. Seither unterstützen viele moderne Programmiersprachen anonyme Funktionen.

Lange mussten Java-Entwickler auf diese Funktionalität verzichten und stattdessen auf anonyme Klassen zurückgreifen. Diese verfolgen zwar ein ähnliches Konzept, führen aber häufig zu überladendem und schlecht lesbarem Code. Mit dem JDK 8 sind Lambda Expressions nun auch in Java angekommen.

Lambda Expressions sollen aber nicht nur zu übersichtlicherem und verständlichem Code führen, sondern bringen durch die Erweiterung des Collection Framework ganz neue Möglichkeiten im Umgang mit Aufzählungselementen mit sich. Deren Verarbeitung kann einfacher parallelisiert und Operationen können erst bei Bedarf durchgeführt werden.

Anonyme Klassen

Bevor wir uns näher mit den Lambda Expressions beschäftigen, soll zunächst auf die Funktionsweise und Verwendung von anonymen Klassen eingegangen werden. Anonyme Klassen sind ein Spezialfall der lokalen Klassen. Sie finden häufig dann Verwendung, wenn ein Interface implementiert werden soll, das nur an einer einzigen Stelle Verwendung findet und eine eigene Klasse zu aufwändig wäre.

Die Implementierung einer anonymen Klasse sollte allerdings kurz und einfach gehalten werden. Im Gegensatz zu anderen Klassenarten werden anonyme Klassen im Kontext von Ausdrücken definiert. Ein Objekt einer anonymen Klasse wird nur erzeugt, wenn der entsprechende Code auch durchlaufen wird und die Klasse mit new instanziiert wird.

Eine anonyme Klasse wird bei der Verwendung nicht nur definiert, sondern zeitgleich instanziiert. Das dabei erzeugte Objekt dient als Parameter der Methode. Abbildung 1 veranschaulicht das Interface Comparator und die Implementierung der Methode compare() in Form einer anonymen Klasse.

// Anonyme Klasse

Collections.sort(liste, new Comparator<NatuerlichePerson>() {

   @Override

   public int compare(NatuerlichePerson person1, NatuerlichePerson person2) {

       return person1.getName().compareTo(person2.getName());

   }

});

Abb. 1: Anonyme Klasse

Einen Ausschnitt des Interface Comparator aus dem Package java.util Abbildung 2 aufgezeigt. Bei der Implementierung solcher Interfaces werden häufig anonyme Klassen verwendet.

@FunctionalInterface
public interface Comparator {

     int compare(T o1, T o2);

     ...

}

Abb. 2: Funktionales Interface Comparator.

Darüber hinaus verdeutlicht die Abbildung 2 bereits, dass Interfaces mit der neuen Java-Version mittels der Annotation @FunctionalInterface kenntlich gemacht werden können. Der Compiler der IDE kann so direkt prüfen, ob es sich bei dem annotierten Interface um ein valides funktionales Interface handelt. Ein Interface ist dann ein funktionales Interface, wenn es in der Signatur nur eine einzige Methode deklariert. Darüber hinaus können Interfaces in Java 8 auch Default-Methoden enthalten.

Wenn eine Klasse ein Interface implementiert, müssen - wie bisher - die abstrakten Methoden des Interface überschrieben werden. Das Überschreiben von Default- Methoden ist optional. Abbildung 3 zeigt ein funktionales Interface, welches die Default-Methode timeFormatter() implementiert, die das übergebene Objekt LocalDateTime als String im Format „<Jahr>-<Monat>-<Tag>“ (z.B. 2011- 12-03) zurückliefert.

@FunctionalInterface
public interface MyFunctionalInterface {

     void takeTime(LocalDateTime time);

     default String timeFormatter(LocalDateTime time) {
         return time.format(DateTimeFormatter.ISO_DATE);
     }

}

Abb. 3: Default-Methoden in funktionalen Interfaces.

Im Gegensatz zur Implementierung einer anonymen Klasse, ermöglichen Lambda Expressions eine alternative und kompakte Schreibweise. Funktionale Interfaces sind somit die Voraussetzung für die Verwendung von Lambda Expressions.

In der Java-Klassenbibliothek existieren bereits eine Vielzahl solcher funktionalen Interfaces und mit Java 8 sind noch weitere hinzugekommen. Somit bieten sich viele Anwendungsmöglichkeiten für Lambda Expressions.

Syntax und Aufbau von Lambda Expressions

Die Implementierung von anonymen Klassen kann schnell zu schwer lesbarem Java Code führen. In den meisten Fällen ist die Funktionalität der anonymen Klasse sehr speziell und man wünscht sich, einfach diese Funktionalität übergeben zu können. Stattdessen muss sie aber in Form einer anonymen Klasse und dem daraus resultierenden Objekt zur Verfügung gestellt werden. Lambda Expressions ermöglichen und vereinfachen den Umgang mit Funktionen als Methodenparameter deutlich.

Syntaktisch besteht eine Lambda Expression aus einer Parameterliste und einem Methodenrumpf.

Mittels des Operator -> werden Parameterliste und Methodenrumpf miteinander verknüpft. Ein Ausdruck ist dann zuweisungskompatibel, wenn zu einem funktionalen Interface mit nur einer Methode die korrekte Anzahl an Parametern und der passende Ergebnistyp verwendet wird.

Ein Beispiel des direkten Vergleichs zwischen einer anonymen Comparator-Implementierung und einer Lambda Expression zeigt die Abbildung 4.

String[] names = {"Thomas", "Sebastian", "Christoph","Tobias", "Andreas", "Robert", "Hubert"};

//Anonyme Klasse
Arrays.sort(names, new Comparator() {

     @Override
     public int compare(String name1, String name2) {
          return name1.compareTo(name2);
     }
});

//Lambda Expression
Arrays.sort(names, ((name1, name2) -> name1.compareTo(name2)));
 

Abb. 4: Anonyme Klasse vs. Lambda Expression.

Eine Lambda Expression kann auch mehrere Anweisungen enthalten. Diese müssen dann aber mittels geschweifter Klammern in einen Block gesetzt werden. In der Abbildung 5 ist eine solche Lambda Expression anhand der Methode sort dargestellt. Zunächst wird der Nachname und anschließend der Vorname des Objektes Person verglichen und entsprechend sortiert.

// Lambda Expression
Collections.sort(liste, ((person1, person2) -> {
     int c = person1.getNachname().compareTo(person2.getNachname());
     if (c == 0) {
          c = person1.getVorname().compareTo(person2.getVorname());
     }
     return c;
}));
 

Abb. 5: Mehrzeilige Lambda Expression.

Der Compiler ist in der Lage, aus dem Kontext den Datentyp der Parameter einer Lambda Expression selbstständig zu erkennen. Das Voranstellen der Typenbezeichnung ist somit nicht erforderlich.

Der Methodenrumpf einer Lambda Expression kann direkt auf alle lokalen und privaten Variablen zugreifen, die in der umgebenden Toplevel-Klasse und in derselben Code- Ebene definiert sind. Die lokalen Variablen müssen final deklariert sein oder sich zumindest so verhalten, sprich effektiv unveränderlich sein. Das heißt, dass die Variablen nach der ersten Zuweisung nicht mehr verändert werden. Technisch gelten hier die gleichen Einschränkungen wie auch bei anonymen Klassen. Der Modifier final muss mit Java 8 jedoch nicht mehr explizit angegeben werden.

Der Einsatz von Lambda Expressions hat neben vereinfachtem und kürzerem Code den weiteren Vorteil, dass im Gegensatz zu anonymen Klassen keine zusätzlichen Bytecode-Dateien erzeugt werden.

Gültigkeitsbereiche

Lambda Expressions definieren keinen eigenen Gültigkeitsbereich wie lokale und anonyme Klassen. Der Gültigkeitsbereich ist hier Teil der Definitionsumgebung und übernimmt deren Variablen. Dies hat zur Folge, dass die Parameter und lokalen Variablen einer Lambda Expression mit lokalen Variablen der umliegenden Methode kollidieren. Anders als bei anonymen Klassen bezieht sich das this auf die Definitionsumgebung und referenziert nicht auf die Instanz der Klasse innerhalb des Ausdrucks. Ähnliches gilt für den Aufruf super, welcher in diesem Fall auf die Oberklasse der Basisklasse verweist, in welcher die Lambda Expression definiert wird

Auch die Anweisungen break und continue können durchaus innerhalb einer Lambda Expression zur Steuerung des internen Kontrollflusses verwendet werden.

Referenzen auf Methoden

In einigen Fällen führt eine Lambda Expression nur einen Aufruf auf eine bestimmte Methode aus. Hier ist es oftmals kürzer und übersichtlicher die Lambda Expression durch eine Method Reference zu ersetzen. Die Syntax einer Method Reference für statische Methoden definiert sich über Klasse::statischeMethode und übergibt eine Referenz auf eine Methode. Auch nicht-statische Methoden von Objekten lassen sich als Parameter übergeben. In diesem Fall lautet die Schreibweise objektvariable::instanzMethode . Alternativ bietet Java hierfür die folgende Schreibweise an, die der ersten Variante ähnelt: Klasse::instanzMethode.

Syntaktisch besteht eine Method Reference folglich aus einem Receiver und dem Namen der statischen Methode oder der Instanzmethode.

Receiver und Methodenname werden durch das Symbol „::“ verbunden. Der Receiver wird dabei stets vor den zwei Doppelpunkten und der Methodenname jeweils dahinter angegeben. Der Receiver kann einem Objekt oder aber einem Typ entsprechen. Eine Methode unter Verwendung einer Method Reference wird nicht aufgerufen sondern referenziert.

In unserem Beispiel aus Abbildung 6 wird sort() eine Referenz auf die Methode compareTo() der Klasse String übergeben. Alternativ lässt sich das Beispiel aus Abbildung 4 auch mittels einer Method Reference umsetzten.

String[] names = {"Thomas", "Sebastian", "Christoph","Tobias", "Andreas", "Robert", "Hubert"};

// Lambda Expression
Arrays.sort(names, ((name1, name2) -> name1.compareTo(name2)));

// Method Reference
Arrays.sort(names,String::compareTo);
 

Abb. 6: Method Reference.

Hinter der Method Reference verbirgt sich semantisch nichts anders als die folgende Lambda Expression:

(name1, name2) -> String.compareTo(name1, name2)

Fazit und Ausblick

Lambda Expressions sind eine neue syntaktische Struktur in Java, an die man sich zunächst gewöhnen muss. Dennoch können Lambda Expressions und Method References dazu beitragen, Java Code übersichtlicher zu gestalten. Die Implementierung von anonymen Klassen kann bereits in vielen Fällen durch Lambda Expressions oder sogar Method References ersetzt werden.

Darüber hinaus ermöglichen Erweiterungen der Collection API im Zusammenhang mit Lambda Expressions vereinfachte Möglichkeiten zur parallelen Datenverarbeitung.

Nachdem dieser Artikel die Funktionsweise von Lambda Expressions vorgestellt hat, werden sich die Folgeartikel dieser Reihe mit weiteren Neuerungen in Java 8 beschäftigen. Sie können sich auf Themen wie Stream API - Parallele Algorithmen, Date & Time API und IO/NIO freuen.