Zum Inhalt
wannacode

Lambdas

Kurz gesagt: Ein Lambda ist eine kurze Funktion ohne Namen, die du als Wert übergibst, zum Beispiel zum Sortieren oder Filtern; in Java implementiert es ein funktionales Interface.

Problem

Sechs Zeilen für einen Vergleich

Die Bestellungen sollen nach Summe sortiert werden. Wie das geht, weißt du aus dem Modul „Collections“: sort mit einem Comparator, geschrieben als anonyme Klasse.

Order
class Order {
private String customer;
private double total;
public Order(String customer, double total) {
this.customer = customer;
this.total = total;
}
public String getCustomer() {
return customer;
}
public double getTotal() {
return total;
}
}
List<Order> orders = new ArrayList<>(List.of(
new Order("Maria", 75.50),
new Order("Tom", 12.90),
new Order("Laura", 40.00)));
orders.sort(new Comparator<Order>() {
@Override
public int compare(Order a, Order b) {
return Double.compare(a.getTotal(), b.getTotal());
}
});
for (Order o : orders) {
System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());
}
Ausgabe
Tom: 12.90 €
Laura: 40.00 €
Maria: 75.50 €

Es funktioniert. Aber von den sechs Zeilen der anonymen Klasse sagt nur eine etwas: Double.compare(a.getTotal(), b.getTotal()). Der Rest ist Verpackung, die du jedes Mal schreibst: new Comparator<Order>(), @Override, public int compare, Klammern. Du willst nur die Regel übergeben.

Die Idee

Ein Lambda: nur die Regel

Dieselbe Sortierung in einer Zeile:

orders.sort((a, b) -> Double.compare(a.getTotal(), b.getTotal()));

(a, b) -> Double.compare(a.getTotal(), b.getTotal()) heißt Lambda-Ausdruck, kurz Lambda. Links von -> stehen die Parameter, rechts der Rumpf. Das ist die Methode compare ohne Namen, ohne Typen und ohne return.

Ein Lambda kann man auf drei Arten schreiben:

  • Ein einzelner Parameter darf ohne Klammern stehen: o -> o.getTotal() > 50. Ohne Parameter ist es () -> ....
  • Die Typen der Parameter lässt man meist weg, weil der Compiler sie von sort kennt. Du kannst sie auch hinschreiben: (Order a, Order b) -> ....
  • Ein Rumpf mit mehreren Zeilen steht in { }, und dann ist return Pflicht.
Comparator<Order> byTotal = (a, b) -> Double.compare(a.getTotal(), b.getTotal());
Comparator<Order> byTotalLong = (Order a, Order b) -> {
double diff = a.getTotal() - b.getTotal();
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
};
orders.sort(byTotalLong);
for (Order o : orders) {
System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());
}
Ausgabe
Tom: 12.90 €
Laura: 40.00 €
Maria: 75.50 €
Ein Lambda ist ein Wert: Du kannst es in eine Variable legen und später übergeben.

Die Idee

Funktionale Interfaces

Woher weiß Java, dass (a, b) -> ... die Methode compare ist? Aus dem Typ: sort erwartet einen Comparator, und der hat eine abstrakte Methode. Das Lambda wird ihr Rumpf.

Ein Interface mit genau einer abstrakten Methode heißt funktionales Interface. Du kannst dir ein eigenes machen:

interface PriceRule {
double apply(double price);
}
PriceRule student = price -> price * 0.8;
PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));
System.out.println(minusFive.apply(50));
Ausgabe
40.0
45.0
student ist ein Objekt vom Typ PriceRule. Du rufst es mit dem Namen der Methode aus dem Interface auf: student.apply(50). Zwei Preisregeln und keine einzige Klasse, die sie implementiert.

PriceRule student = price -> price * 0.8;
PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));
System.out.println(minusFive.apply(50));
Schritt für Schritt, in Worten
  1. Das Lambda ist ein Objekt vom Typ PriceRule, und student zeigt darauf. Sein Rumpf läuft noch nicht.
  2. Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
  3. student.apply(50) übergibt 50 an das Lambda: price ist 50, und price * 0.8 ergibt 40.0.
  4. minusFive.apply(50): dieselbe Zahl, eine andere Regel, 45.0.
Genauer, für Neugierige
  • Die Annotation @FunctionalInterface über dem Interface lässt den Compiler prüfen, dass es genau eine Methode gibt. Fügst du eine zweite abstrakte Methode hinzu:
@FunctionalInterface
interface PriceRule {
double apply(double price);
String name();
}
Fehler
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
PriceRule is not a functional interface
multiple non-overriding abstract methods found in interface PriceRule
  • Methoden mit Rumpf (default und static) zählen nicht. Die Methoden von Object auch nicht: Comparator deklariert boolean equals(Object obj), ist aber trotzdem funktional.
  • Ein Lambda ist keine anonyme Klasse. In ihm ist this das Objekt, in dem es geschrieben ist, nicht das Lambda selbst.

Die Idee

Die fertigen: Predicate, Function, Consumer, Supplier

Du musst nicht für jede Regel ein Interface schreiben. java.util.function hat fertige für die häufigsten Fälle:

  • Predicate<T>: nimmt ein T, gibt einen boolean zurück. Die Methode ist test. Das ist eine Bedingung: „Ist die Bestellung groß?“.
  • Function<T, R>: nimmt ein T, gibt ein R zurück. Die Methode ist apply. Sie macht aus einem etwas anderes: aus einer Bestellung ein Etikett.
  • Consumer<T>: nimmt ein T, gibt nichts zurück. Die Methode ist accept. Er tut etwas damit: gibt es aus, speichert es.
  • Supplier<T>: nimmt nichts, gibt ein T zurück. Die Methode ist get. Er gibt dir ein neues Objekt, wenn du fragst.
Predicate<Order> big = o -> o.getTotal() >= 50;
Function<Order, String> label = o -> o.getCustomer().toUpperCase();
Consumer<String> print = text -> System.out.println("-> " + text);
Supplier<Order> guest = () -> new Order("Gast", 0);
Order order = new Order("Maria", 75.50);
System.out.println(big.test(order));
print.accept(label.apply(order));
System.out.println(guest.get().getCustomer());
Ausgabe
true
-> MARIA
Gast
Die fertigen Interfaces haben auch Methoden, die Regeln zusammensetzen. Prädikate verbindet man mit and, or und negate („nicht“). Funktionen reiht man mit andThen hintereinander.
Genauer, für Neugierige
  • Es gibt Varianten für zwei Parameter: BiFunction<T, U, R>, BiPredicate<T, U>, BiConsumer<T, U>. UnaryOperator<T> ist Function<T, T>, und BinaryOperator<T> ist BiFunction<T, T, T>.
  • Für die primitiven Typen gibt es IntPredicate, IntFunction<R>, ToDoubleFunction<T> und andere. Mit ihnen wird ein int nicht bei jedem Aufruf in ein Integer gepackt.
  • f.andThen(g) macht zuerst f, dann g. f.compose(g) macht es umgekehrt: zuerst g, dann f.

Probier es aus

Zusammengesetzte Bedingungen

Was gibt das Programm aus?

Predicate<Order> big = o -> o.getTotal() >= 50;
Predicate<Order> maria = o -> o.getCustomer().equals("Maria");
Order small = new Order("Maria", 30);
Order large = new Order("Tom", 80);
System.out.println(big.and(maria).test(small));
System.out.println(big.or(maria).test(small));
System.out.println(big.negate().test(large));
Antwort

Sieh es dir an

Listen und Maps nehmen Lambdas

Collections haben Methoden, die statt einer Schleife eine Regel nehmen:
List<String> cart = new ArrayList<>(List.of("Brot", "", "Milch", " ", "Kaffee"));
cart.removeIf(item -> item.isBlank());
cart.forEach(item -> System.out.println(item));
Map<String, List<String>> byCity = new TreeMap<>();
byCity.computeIfAbsent("Berlin", city -> new ArrayList<>()).add("Maria");
byCity.computeIfAbsent("Hamburg", city -> new ArrayList<>()).add("Tom");
byCity.computeIfAbsent("Berlin", city -> new ArrayList<>()).add("Laura");
byCity.forEach((city, names) -> System.out.println(city + ": " + names));
Ausgabe
Brot
Milch
Kaffee
Berlin: [Maria, Laura]
Hamburg: [Tom]
  • removeIf(bedingung) entfernt alle Elemente, für die die Bedingung wahr ist. Das ist ein Predicate.
  • forEach(aktion) führt die Aktion mit jedem Element aus. Das ist ein Consumer.
  • computeIfAbsent(schlüssel, funktion) gibt den Wert für den Schlüssel zurück. Gibt es keinen, ruft es die Funktion auf, legt das Ergebnis in die Map und gibt es zurück. So braucht das Gruppieren nach Stadt eine Zeile, ohne if (!byCity.containsKey(...)).
  • Das forEach der Map nimmt ein Lambda mit zwei Parametern: den Schlüssel und den Wert.

TreeMap hält die Schlüssel sortiert, deshalb kommt Berlin vor Hamburg.

Die Idee

Methodenreferenzen: ::

Oft gibt ein Lambda seinen Parameter nur an eine vorhandene Methode weiter: s -> System.out.println(s). Dann kannst du einfach schreiben, welche Methode es ist:
List<String> names = List.of("maria", "tom");
names.forEach(System.out::println);
Function<String, String> upper = String::toUpperCase;
Function<String, Integer> parse = Integer::parseInt;
Supplier<List<String>> newList = ArrayList::new;
System.out.println(upper.apply("laura"));
System.out.println(parse.apply("42") + 1);
List<String> list = newList.get();
list.add("Kaffee");
System.out.println(list);
Ausgabe
maria
tom
LAURA
43
[Kaffee]

Das heißt Methodenreferenz. Es gibt vier Arten:

  • eine Methode eines bestimmten Objekts: System.out::println ist s -> System.out.println(s);
  • eine Methode des Parameters: String::toUpperCase ist s -> s.toUpperCase();
  • eine statische Methode: Integer::parseInt ist s -> Integer.parseInt(s);
  • ein Konstruktor: ArrayList::new ist () -> new ArrayList<>().

Tut das Lambda etwas mehr, zum Beispiel s -> s.trim().toUpperCase(), lass es als Lambda stehen.

Sieh es dir an

Sortieren nach mehreren Feldern

Comparator.comparing nimmt eine Funktion, die den Schlüssel für den Vergleich herauszieht, und macht daraus einen Comparator. Die nächste Regel hängst du mit thenComparing an, und reversed() dreht die Richtung um.

Die Schüler werden nach Note sortiert, von der höchsten an, und bei gleicher Note nach Name:

List<Student> students = new ArrayList<>(List.of(
new Student("Jonas", 5.50),
new Student("Anna", 6.00),
new Student("Peter", 5.50),
new Student("Finn", 4.75)));
students.sort(Comparator.comparing(Student::getGrade).reversed()
.thenComparing(Student::getName));
Ausgabe
Anna 6.0
Jonas 5.5
Peter 5.5
Finn 4.75

Du liest es von links nach rechts: „vergleiche nach Note, dreh um, dann bei Gleichheit vergleiche nach Name“. Kein einziges if, keine einzige anonyme Klasse.

Genauer, für Neugierige
  • Mit einem Lambda statt einer Referenz lässt sich reversed() manchmal nicht kompilieren:
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());
Fehler
Main.java:26: error: cannot find symbol
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());
^
symbol: method getTotal()
location: variable o of type Object

Der Compiler ermittelt den Typ von o von innen nach außen. Wenn er bei .reversed() ankommt, weiß er noch nicht, dass es um Order geht, und nimmt Object an. Die Lösung ist eine Referenz oder ein Typ am Parameter:

orders.sort(Comparator.comparing(Order::getTotal).reversed());
orders.sort(Comparator.comparing((Order o) -> o.getTotal()).reversed());
  • comparingInt und comparingDouble machen dasselbe, ohne in Integer und Double zu packen.
  • Comparator.naturalOrder() und Comparator.reverseOrder() sortieren nach der natürlichen Reihenfolge des Typs: Zahlen nach Größe, Text alphabetisch.

Probier es aus

Was kehrt reversed() um?

Hier steht reversed() am Ende, nach thenComparing.

Was gibt das Programm aus?

List<Student> students = new ArrayList<>(List.of(
new Student("Jonas", 5.50),
new Student("Anna", 6.00),
new Student("Peter", 5.50),
new Student("Finn", 4.75)));
students.sort(Comparator.comparing(Student::getGrade)
.thenComparing(Student::getName)
.reversed());
students.forEach(s -> System.out.println(s.getName() + " " + s.getGrade()));
Antwort

Probier es aus

Finde den Fehler

Das Programm soll die Summe der Preise ausgeben. Es lässt sich nicht kompilieren. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Genauer, für Neugierige

So eine Variable zu lesen ist erlaubt:

double rate = 0.2;
PriceRule discount = price -> price * (1 - rate);
System.out.println(discount.apply(100));
Ausgabe
80.0

Die Felder eines Objekts sind keine lokalen Variablen, und ein Lambda darf sie ändern. Das ist kein Weg um die Regel herum: Laufen Lambdas gleichzeitig in mehreren Threads, ist ein gemeinsames Feld eine Quelle von Fehlern. Threads kommen im Modul „Programme in der echten Welt“.

Übung

Regeln als Werte

Löse die Aufgaben in deinem Editor. Für Predicate, Function und die anderen füge import java.util.function.*; hinzu.

1. Sortieren nach mehreren Feldern. Eine Klasse Product mit Name, Kategorie und Preis. Sortiere die Produkte nach Kategorie, innerhalb einer Kategorie vom teuersten an und bei gleichem Preis nach Name. Die Produkte sind: Maus, Zubehör, 25 €; Laptop, Computer, 1299 €; Tastatur, Zubehör, 49,90 €; Tablet, Computer, 399 €; Kopfhörer, Zubehör, 49,90 €.

Ausgabe
Computer | Laptop | 1299.00 €
Computer | Tablet | 399.00 €
Zubehör | Kopfhörer | 49.90 €
Zubehör | Tastatur | 49.90 €
Zubehör | Maus | 25.00 €

2. Bestellungen filtern. Gib Order eine Stadt. Schreib eine Methode select(List<Order> orders, Predicate<Order> rule), die eine neue Liste nur mit den Bestellungen zurückgibt, für die die Regel wahr ist. Mach zwei Regeln, hamburg und big (mindestens 50 €), und gib die Bestellungen mit hamburg.and(big), mit hamburg.negate() und mit „Hamburg oder mindestens 100 €“ aus. Die Bestellungen: Maria, Hamburg, 75,50 €; Tom, Dresden, 12,90 €; Laura, Hamburg, 40 €; Lukas, Berlin, 120 €.

Ausgabe
Hamburg, ab 50 €:
Maria, Hamburg: 75.50 €
Außerhalb von Hamburg:
Tom, Dresden: 12.90 €
Lukas, Berlin: 120.00 €
Kostenlose Lieferung (Hamburg oder ab 100 €):
Maria, Hamburg: 75.50 €
Laura, Hamburg: 40.00 €
Lukas, Berlin: 120.00 €
3. Rabatte nacheinander. Mach drei Rabatte als Function<Double, Double>: einen Studentenrabatt (−10 %), einen Black-Friday-Rabatt (−20 €) und „nicht unter null“. Verkette sie mit andThen in zwei Reihenfolgen: zuerst den Studentenrabatt und zuerst den Freitagsrabatt. Probier es mit 120 € und mit 15 €. Spielt die Reihenfolge eine Rolle?
Ausgabe
120.00 € -> Student, dann Freitag: 88.00 €; Freitag, dann Student: 90.00 €
15.00 € -> Student, dann Freitag: 0.00 €; Freitag, dann Student: 0.00 €

Weitere Aufgaben

  • Eine Liste aufräumen. Entferne aus einer Liste von E-Mail-Adressen die leeren und die ohne @ mit einem einzigen removeIf und gib den Rest mit forEach(System.out::println) aus.
  • Schüler nach Klasse. Gruppiere Schüler nach Klasse ("10A", "10B") in einer TreeMap<String, List<String>> mit computeIfAbsent und gib die Map mit forEach aus.
  • Dein eigenes Interface. Mach @FunctionalInterface interface Validator { boolean check(String value); } und drei Validatoren als Lambdas: nicht leerer Text, nur Ziffern, Länge bis 10. Probier sie an ein paar Strings aus.

Test

Teste dich

1. Wo kann ein Lambda-Ausdruck stehen?
2. Welches fertige Interface nimmt eine Bestellung und gibt true oder false zurück?
3. Wodurch kann man das Lambda s -> s.toUpperCase() ersetzen?
4. Comparator.comparing(Student::getGrade).thenComparing(Student::getName).reversed(). Was kehrt reversed() um?
5. Warum lässt sich int total = 0; list.forEach(x -> total += x); nicht kompilieren?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·