EMZETT.
Login

Kurz: Ein Lambda ist eine kurze, namenlose Funktion. Sie passt überall, wo ein funktionales Interface (genau eine abstrakte Methode) erwartet wird:

Teil des Kurses Java

Kapitel 16 von 22 im Kurs Java. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Lambda-Ausdrücke

Ein Lambda ist eine kurze, namenlose Funktion. Sie passt überall, wo ein funktionales Interface (genau eine abstrakte Methode) erwartet wird:

import java.util.*;
import java.util.function.*;
 
public class Lambdas {
    public static void main(String[] args) {
        Function<Integer, Integer> quadrat = x -> x * x;
        BiFunction<Integer, Integer, Integer> plus = (a, b) -> a + b;
        Predicate<String> leer = s -> s.isEmpty();
        Supplier<String> lieferant = () -> "Wert";
        Consumer<String> ausgabe = s -> System.out.println("> " + s);
        UnaryOperator<String> gross = String::toUpperCase;           // Methodenreferenz
 
        System.out.println(quadrat.apply(7) + " " + plus.apply(3, 4) + " " + leer.test("") + " " + lieferant.get());
        ausgabe.accept(gross.apply("hallo"));
        System.out.println(quadrat.andThen(x -> x + 1).apply(5));    // verketten
        System.out.println(leer.negate().test("x"));
 
        List<String> namen = new ArrayList<>(List.of("Zoe", "Anna", "Tom"));
        namen.sort((a, b) -> a.length() - b.length());
        namen.forEach(n -> System.out.print(n + " "));
        System.out.println();
    }
}

Ausgabe:

49 7 true Wert
> HALLO
26
true
Zoe Tom Anna
InterfaceMethodeBedeutung
Function<T, R>R apply(T)wandelt um
Predicate<T>boolean test(T)prüft
Consumer<T>void accept(T)verarbeitet
Supplier<T>T get()liefert
BiFunction<T, U, R>R apply(T, U)zwei Eingaben
UnaryOperator<T>, BinaryOperator<T>gleiche Typen
Runnable, Callable<V>run(), call()Aufgaben

Methodenreferenzen sind Kurzformen: String::length, System.out::println, Person::new, this::methode.

Streams

Ein Stream beschreibt eine Pipeline für Daten: Quelle → Zwischenoperationen → Endoperation. Das Original bleibt unverändert.

import java.util.*;
import java.util.stream.*;
 
public class Pipeline {
    record Person(String name, int alter, String stadt) {}
 
    public static void main(String[] args) {
        List<Person> leute = List.of(
            new Person("Mia", 17, "Köln"), new Person("Tom", 19, "Berlin"),
            new Person("Zoe", 21, "Köln"), new Person("Ben", 15, "Bonn"));
 
        List<String> volljaehrig = leute.stream()
            .filter(p -> p.alter() >= 18)
            .map(Person::name)
            .sorted()
            .collect(Collectors.toList());
        System.out.println(volljaehrig);
 
        double schnitt = leute.stream().mapToInt(Person::alter).average().orElse(0);
        System.out.println(schnitt + " " + leute.stream().mapToInt(Person::alter).max().getAsInt());
 
        System.out.println(leute.stream().anyMatch(p -> p.stadt().equals("Bonn")) + " " +
                           leute.stream().allMatch(p -> p.alter() > 16) + " " +
                           leute.stream().filter(p -> p.alter() > 100).findFirst().isPresent());
        System.out.println(IntStream.rangeClosed(1, 5).boxed().collect(Collectors.toList()));
        System.out.println(Stream.of("a", "b", "c").collect(Collectors.joining(", ", "[", "]")));
    }
}

Ausgabe:

[Tom, Zoe]
18.0 21
true false false
[1, 2, 3, 4, 5]
[a, b, c]

Typische Operationen

ZwischenoperationWirkung
filter(p)behält passende Elemente
map(f)wandelt jedes Element um
flatMap(f)wandelt und flacht ein
sorted(), sorted(cmp)sortiert
distinct()entfernt Duplikate
limit(n), skip(n)begrenzt / überspringt
peek(c)Blick hinein (zum Debuggen)
EndoperationWirkung
collect(...)sammelt in Liste, Set, Map, …
forEach(c)wendet an
count(), sum(), average(), min, maxAuswertungen
reduce(init, op)faltet zu einem Wert
anyMatch, allMatch, noneMatchprüfen
findFirst(), findAny()ein Element (Optional)
toList()unveränderliche Liste (ab Java 16)

Gruppieren und Zusammenfassen

Collectors bietet mächtige Auswertungen:

import java.util.*;
import java.util.stream.*;
 
public class Gruppen {
    record Person(String name, int alter, String stadt) {}
 
    public static void main(String[] args) {
        List<Person> leute = List.of(
            new Person("Mia", 17, "Köln"), new Person("Tom", 19, "Berlin"),
            new Person("Zoe", 21, "Köln"), new Person("Ben", 15, "Bonn"));
 
        Map<String, List<String>> nachStadt = leute.stream()
            .collect(Collectors.groupingBy(Person::stadt, TreeMap::new, Collectors.mapping(Person::name, Collectors.toList())));
        System.out.println(nachStadt);
 
        Map<String, Long> anzahl = leute.stream().collect(Collectors.groupingBy(Person::stadt, TreeMap::new, Collectors.counting()));
        System.out.println(anzahl);
 
        Map<Boolean, List<String>> geteilt = leute.stream()
            .collect(Collectors.partitioningBy(p -> p.alter() >= 18, Collectors.mapping(Person::name, Collectors.toList())));
        System.out.println(geteilt);
 
        Map<String, Integer> alterMap = leute.stream().collect(Collectors.toMap(Person::name, Person::alter));
        System.out.println(new TreeMap<>(alterMap));
 
        IntSummaryStatistics st = leute.stream().mapToInt(Person::alter).summaryStatistics();
        System.out.println(st.getMin() + " " + st.getMax() + " " + st.getAverage() + " " + st.getSum());
 
        System.out.println(leute.stream().map(Person::alter).reduce(0, Integer::sum));
        System.out.println(leute.stream().sorted(Comparator.comparingInt(Person::alter).reversed()).map(Person::name).toList());
    }
}

Ausgabe:

{Berlin=[Tom], Bonn=[Ben], Köln=[Mia, Zoe]}
{Berlin=1, Bonn=1, Köln=2}
{false=[Mia, Ben], true=[Tom, Zoe]}
{Ben=15, Mia=17, Tom=19, Zoe=21}
15 21 18.0 72
72
[Zoe, Tom, Mia, Ben]

Streams sind lazy

Zwischenoperationen laufen erst, wenn eine Endoperation sie anstößt. Das erlaubt auch unendliche Streams:

import java.util.stream.*;
 
public class Faul {
    public static void main(String[] args) {
        var ergebnis = Stream.iterate(1, n -> n * 2)         // 1, 2, 4, 8, ... unendlich
            .peek(n -> System.out.print("[" + n + "]"))
            .filter(n -> n > 10)
            .limit(2)
            .toList();
        System.out.println();
        System.out.println(ergebnis);
        System.out.println(IntStream.range(0, 20).filter(n -> n % 3 == 0).sum());
    }
}

Ausgabe:

[1][2][4][8][16][32]
[16, 32]
63

Hinweis

Ein Stream lässt sich nur einmal benutzen. Für Rechenarbeit auf Listen sind Streams bequem, eine einfache for-Schleife ist manchmal klarer und schneller. Entscheide nach Lesbarkeit.

Parallele Streams

list.parallelStream() verteilt die Arbeit auf mehrere Kerne. Sinnvoll nur bei vielen Elementen und rechenintensiven, nebenwirkungsfreien Schritten. Messen, bevor du es einsetzt.

Merke

  • Lambdas (a, b) -> a + b und Methodenreferenzen String::length ersetzen kleine Klassen
  • Funktionale Interfaces: Function, Predicate, Consumer, Supplier
  • Stream = Quelle → filter/map/sorted → Endoperation (collect, count, reduce)
  • Collectors.groupingBy, partitioningBy, toMap, joining für Auswertungen
  • Streams sind lazy und einmal verwendbar

Übungsaufgabe

Gegeben eine Liste von Bestellungen (Kunde, Betrag): Berechne mit Streams den Umsatz je Kunde und gib die Top 3 aus.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Generics

Weiter: Dateien und Ein-/Ausgabe

Alle Kapitel: Java im Überblick