Lambda Expressions
🚧 | Diese Seite befindet sich in Bearbeitung | 🚧 |
Vorab
Lambda Expressions oder auch Lambdas sind nicht Teil des Vorlesungsstoffes und werden daher als nicht trivial angesehen. Außerdem gehören Lambdas eher zum Bereich der funktionalen Programmierung. Im Programmieren-Kurs beschäftigen wir uns jedoch mit objektorientierter Programmierung (OOP). Sie können auch alle Lösungen immer ohne Lambdas lösen und riskieren so nicht den Abzug für zu komplexe Lambda-Ausdrücke.
Das heißt, dass die Benutzung zwar erlaubt ist, allerdings entsprechend diese Stellen im Quellcode genau zu dokumentieren sind.
Motivation
Manchmal benötigen wir innerhalb unserer Klassen zusätzliche Hilfs-Klassen, die allerdings nur dort Verwendung finden. Diese Klassen nennen wir dann lokale Klassen. Diese werden dann wie gewohnt deklariert, mit dem einzigen Unterschied, dass diese nun in einer anderen Klasse "geschachtelt" sind. Sie haben also insbesondere einen Namen und werden auch als wie uns bekannte Klasse verwendet. Wir können dieses Verhalten allerdings noch verkürzter verwenden. So können wir eine Klasse, statt sie als Klasse zu deklarieren, auch als Ausdruck definieren. Diese Art von Klasse implementiert in diesem Fall immer ein Interface und besitzt keinen Namen. Diese Art von Klasse nennen wir dann anonyme Klasse. Wenn wir nun allerdings nur eine Methode einer solchen Klasse benötigen, wollen wir in der Regel Funktionalität als Argument einer Methode übergeben. In diesem Fall sind die oben genannten Methoden zu aufwendig, was uns zu dem Thema dieses Artikels führt:
Zunächst die Syntax
Jede Lambda Expression besteht aus drei Dingen:
- Einer Liste der Parameter die benötigt werden, die durch Kommata getrennt ist und in runden Klammern steht (Gibt es nur einen Parameter, so kann der Datentyp und die Klammern weggelassen werden)
- dem "Arrow-Token", also ein Pfeil "->" und
- einem Körper, welcher aus einer oder mehrerer Zeilen Code bestehen kann. Besteht der Körper aus mehreren Zeilen Code, so muss dieser, wie ein Methodenkörper, mit geschweiften Klammern gruppiert werden. Auch muss in diesem dann ein return Statement existieren (auch hier gibts es wieder eine Ausnahme, diesmal für Einzeiler: Hier wird kein return Statement benötigt)
Ein Beispiel:
(Person p) -> p.getAge() >= 18
&& p.getAge() <= 25
Diese Lambda Expression prüft (wie auch in dem Beispiel gleich) ob eine Person zwischen 18 und 25 Jahre alt ist. Auch valide Syntax wäre in diesem Fall:
p -> {
return p.getAge() >= 18
&& p.getAge() <= 25
}
Ein praxisnahes Beispiel
Stellen wir uns vor, wir haben folgende Klasse:
public class Person {
String name;
LocalDate birthday;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
Wir wollen nun die Möglichkeit haben, Personen nach verschiedenen Kriterien zu filtern und auszugeben.
Wir implementieren die Funktionalität auf verschiedene Weisen zur Veranschaulichung, endend mit Lambdas:
Einzelne Methoden
Wir implementieren eine Methode pro Filter, so z.B. eine Methode, um alle Personen auszugeben, die älter als ein gegebenes Alter sind:
public static void printPersonsOlderThan(List\<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
Es sollte schnell ersichtlich sein, dass dieser Ansatz auf verschiedene Weisen nicht gut ist. Wir erstellen sehr restriktive Methoden, durch die wir gezwungen werden neue Methoden zu implementieren um weiterhin gewünschtes Verhalten zu erhalten. Das führt auch unweigerlich zu unnötiger Codedopplung. (Was, wenn wir alle Personen erhalten wollen die jünger als das gewünschte Alter sind?) Zuletzt führt dieses Vorgehen dazu, dass der Quellcode brüchig ("brittle") wird. Also durch zukünftige Updates und Erweiterungen nicht mehr funktionieren wird. Das kann also schnell dazu führen, dass das Projekt von Grund auf neu geschrieben werden muss, um diesen Fehler zu beheben.
Dieser Ansatz führt also nicht zu einer zufriedenstellenden Lösung.
(Dieser Ansatz führt auch dann nicht zu einer zufriedenstellenden Lösung, wenn wir die Methoden etwas generischer gestalten. Z.B. Alter zwischen x und y. Warum? - Was ist mit Kombinationen aus Eigenschaften, nach denen gefiltert werden soll?)
Lokale Klasse
Wir erstellen zunächst eine zusätzliche Methode, die die Personen ausgibt, die einem gegebenen Kriterium entsprechen:
public static void printPersons(List\<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Diese Methode prüft jede gegebene Person in der Liste, ob sie dem in CheckPerson definierten Kriterium entspricht. Dies passiert, indem tester.test() aufgerufen wird. Gibt diese Methode true zurück, wird die Person ausgegeben.
Um nun einen Filter zu erstellen, implementieren wir das folgende CheckPerson interface:
interface CheckPerson {
boolean test(Person p);
}
Wir erstellen jetzt einen Filter, der alle Personen findet, die zwischen 18 und 25 Jahren alt sind:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
Wir haben jetzt einen Filter. Aber wie benutzen wir den jetzt?
Ganz einfach:
printPersons(roster, new CheckPersonEligibleForSelectiveService());
Und was ist der Vorteil?
Ändert sich die Person-Klasse, müssen wir keinen Code umschreiben. Aber:
Wir haben viel zusätzlichen Code. Ein Interface und für jeden Filter eine lokale Klasse.
Anonyme Klasse
Wir können uns viel Schreibarbeit sparen wenn wir statt lokaler, anonyme Klassen verwenden:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
)
Wir müssen dadurch erneut viel weniger Code schreiben.
Aber auch hier gibt es einen Nachteil:
Anonyme Klassen sind sehr klobig und schwer schnell zu lesen. Dafür, dass wir nur eine Methode implementieren, schreiben wir doch sehr viel "Boilerplate" Code.
Wir haben jetzt aber konzeptionell verstanden, was wir vorhaben und können jetzt Lambdas effektiv einsetzen um endlich eine zufriedenstellende Lösung zu finden:
Lambdas
Das CheckPerson Interface ist ein sogenanntes "functional interface", also eins, das nur eine abstrakte Methode enthält.
Wenn wir nur eine Methode implementieren wollen, können wir den Namen weglassen und sind damit bei Lambdas angekommen. Eine Implementation mit diesen sieht dann wiefolgt aus:
printPersons(
roster,
(Person p) -> p.getAge() >= 18 &&
p.getAge() <= 25
)
Wir wollen unser Ergebnis nun noch etwas verfeinern (das JDK hat nämlich noch weitere Hilfen um uns Arbeit abzunehmen):
Functional interfaces sind so einfach, dass die JDK schon sämtliche standard functional interfaces. Diese befinden sich im Paket java.util.function.
Für unser oben eingeführtes Beispiel ist das Predicate\<T> Interface am besten geeignet, da es bereits eine Methode boolean test(T t) definiert.
Wenn wir jetzt den generischen Typ durch, in diesem Fall, Person ersetzen, erhalten wir genau die Funktionalität von oben, nur ohne, dass wir das functional interface selbst implementieren mussten.
Da dieser Artikel das Thema rundum Lambdas nur angebrochen hat lohnt es sich diesen Artikel noch einmal in gänze durchzulesen.