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.
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());}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
sortkennt. Du kannst sie auch hinschreiben:(Order a, Order b) -> .... - Ein Rumpf mit mehreren Zeilen steht in
{ }, und dann istreturnPflicht.
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());}Tom: 12.90 €Laura: 40.00 €Maria: 75.50 €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));40.045.0student 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
- Das Lambda ist ein Objekt vom Typ PriceRule, und student zeigt darauf. Sein Rumpf läuft noch nicht.
- Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
- student.apply(50) übergibt 50 an das Lambda: price ist 50, und price * 0.8 ergibt 40.0.
- 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:
@FunctionalInterfaceinterface PriceRule { double apply(double price); String name();}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 (
defaultundstatic) zählen nicht. Die Methoden vonObjectauch nicht:Comparatordeklariertboolean equals(Object obj), ist aber trotzdem funktional. - Ein Lambda ist keine anonyme Klasse. In ihm ist
thisdas 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 einT, gibt einenbooleanzurück. Die Methode isttest. Das ist eine Bedingung: „Ist die Bestellung groß?“.Function<T, R>: nimmt einT, gibt einRzurück. Die Methode istapply. Sie macht aus einem etwas anderes: aus einer Bestellung ein Etikett.Consumer<T>: nimmt einT, gibt nichts zurück. Die Methode istaccept. Er tut etwas damit: gibt es aus, speichert es.Supplier<T>: nimmt nichts, gibt einTzurück. Die Methode istget. 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());true-> MARIAGastand, 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>istFunction<T, T>, undBinaryOperator<T>istBiFunction<T, T, T>. - Für die primitiven Typen gibt es
IntPredicate,IntFunction<R>,ToDoubleFunction<T>und andere. Mit ihnen wird einintnicht bei jedem Aufruf in einIntegergepackt. f.andThen(g)macht zuerstf, danng.f.compose(g)macht es umgekehrt: zuerstg, dannf.
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));big.and(maria) false, und big.or(maria) ist true, weil sie von Maria ist. Toms Bestellung hat 80 €, sie ist groß, und negate() dreht die Antwort um: false.Sieh es dir an
Listen und Maps nehmen Lambdas
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));BrotMilchKaffeeBerlin: [Maria, Laura]Hamburg: [Tom]removeIf(bedingung)entfernt alle Elemente, für die die Bedingung wahr ist. Das ist einPredicate.forEach(aktion)führt die Aktion mit jedem Element aus. Das ist einConsumer.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, ohneif (!byCity.containsKey(...)).- Das
forEachder 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: ::
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);mariatomLAURA43[Kaffee]Das heißt Methodenreferenz. Es gibt vier Arten:
- eine Methode eines bestimmten Objekts:
System.out::printlnists -> System.out.println(s); - eine Methode des Parameters:
String::toUpperCaseists -> s.toUpperCase(); - eine statische Methode:
Integer::parseIntists -> Integer.parseInt(s); - ein Konstruktor:
ArrayList::newist() -> 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));Anna 6.0Jonas 5.5Peter 5.5Finn 4.75Du 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());Main.java:26: error: cannot find symbol orders.sort(Comparator.comparing(o -> o.getTotal()).reversed()); ^ symbol: method getTotal() location: variable o of type ObjectDer 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());comparingIntundcomparingDoublemachen dasselbe, ohne inIntegerundDoublezu packen.Comparator.naturalOrder()undComparator.reverseOrder()sortieren nach der natürlichen Reihenfolge des Typs: Zahlen nach Größe, Text alphabetisch.
Probier es aus
Was kehrt reversed() um?
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()));reversed() kehrt den Comparator um, auf dem es aufgerufen wird. Der ist schon „nach Note, dann nach Name“, also werden beide umgekehrt: Die Noten gehen von der höchsten an, und Peter kommt vor Jonas. Willst du nur die Noten umgekehrt, steht reversed() direkt nach comparing(Student::getGrade), wie auf der Karte davor.Probier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
Das Lambda ändert total, und das ist nicht erlaubt:
Main.java:9: error: local variables referenced from a lambda expression must be final or effectively final prices.forEach(p -> total += p); ^Ein Lambda kann später und an einer anderen Stelle laufen, deshalb bekommt es eine Kopie des Werts der lokalen Variable. Könnte sich die Variable ändern, würden Kopie und Original auseinanderlaufen. Deshalb erlaubt Java in einem Lambda nur lokale Variablen, die sich nach ihrem ersten Wert nicht mehr ändern. Sie heißen effectively final (effektiv final): Sie funktionieren auch ohne das Wort final. Das gilt auch für eine Änderung außerhalb des Lambdas: Schreibst du irgendwo weiter unten total = 5, kann das Lambda total trotzdem nicht lesen.
Für eine Summe nimm eine normale Schleife. In der nächsten Lektion machst du es auch in einer Zeile.
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));80.0Die 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
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 €.
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 €.
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 €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?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 einzigenremoveIfund gib den Rest mitforEach(System.out::println)aus. - Schüler nach Klasse. Gruppiere Schüler nach Klasse (
"10A","10B") in einerTreeMap<String, List<String>>mitcomputeIfAbsentund gib die Map mitforEachaus. - 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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces · Listen · HashMap und HashSet · Klassen in Klassen
- Geht weiter in: Streams · record, switch und Muster · Entwurfsmuster · Tests mit JUnit
- Aufgaben zum Thema: Modernes Java