Streams
Kurz gesagt: Ein Stream verarbeitet eine Collection als Folge von Schritten: filter siebt aus, map wandelt um, sorted sortiert, und eine terminale Operation wie toList liefert das Ergebnis.
Problem
Ein Bericht in einer Schleife
Die Bestellungen haben jetzt auch eine Stadt:
class Order { private String customer; private String city; private double total;
public Order(String customer, String city, double total) { this.customer = customer; this.city = city; this.total = total; }
public String getCustomer() { return customer; }
public String getCity() { return city; }
public double getTotal() { return total; }}List<Order> orders = List.of( new Order("Maria", "Hamburg", 75.50), new Order("Tom", "Dresden", 12.90), new Order("Laura", "Hamburg", 40.00), new Order("Lukas", "Berlin", 120.00), new Order("Anna", "Dresden", 64.00));Du willst die Kunden mit Bestellungen ab 50 €, alphabetisch sortiert:
List<String> result = new ArrayList<>();for (Order o : orders) { if (o.getTotal() >= 50) { result.add(o.getCustomer()); }}Collections.sort(result);System.out.println(result);[Anna, Lukas, Maria]Acht Zeilen, und die Frage ist nur eine. Der Code sagt, wie: eine leere Liste anlegen, durchlaufen, prüfen, hinzufügen, sortieren. Um zu verstehen, was er tut, musst du ihn im Kopf ausführen. Kommt „nur die ersten drei“ oder „ohne Wiederholungen“ dazu, wächst die Schleife weiter.
Die Idee
Ein Stream: eine Folge von Schritten
Derselbe Bericht, als Schritte geschrieben:
List<String> result = orders.stream() .filter(o -> o.getTotal() >= 50) .map(Order::getCustomer) .sorted() .toList();System.out.println(result);[Anna, Lukas, Maria]Du liest ihn von oben nach unten: Nimm die Bestellungen, behalte die ab 50 €, nimm den Kunden, sortiere, sammle in einer Liste.
orders.stream() macht aus der Liste einen Stream (Datenstrom). Ein Stream speichert keine Daten. Er ist ein Förderband, auf dem die Elemente eins nach dem anderen laufen. Jeder Schritt nimmt ein Lambda aus der vorigen Lektion:
filter(Bedingung)lässt nur die Elemente weiter, für die die Bedingung wahr ist. Das ist einPredicate.map(Funktion)macht aus jedem Element ein anderes. Das ist eineFunction: Sie macht aus einerOrdereinenString.sorted()sortiert nach der natürlichen Reihenfolge,sorted(Comparator)nach deiner Regel.toList()sammelt alles, was am Ende angekommen ist, in einer neuen Liste.
Die Liste orders ändert sich nicht. Der Stream liest sie nur.
Genauer, für Neugierige
filter,map,sortedsind Zwischenoperationen: Jede gibt einen neuen Stream zurück.toList,count,collect,forEachsind terminale Operationen: Sie verlangen ein Ergebnis und starten das Förderband.toList()gibt es seit Java 16, und es gibt eine Liste zurück, die man nicht ändern kann. In älterem Code siehst ducollect(Collectors.toList()). Das macht dasselbe, verspricht aber nicht, welche Art von Liste genau zurückkommt. Brauchst du eineArrayList, die du ändern kannst, schreibcollect(Collectors.toCollection(ArrayList::new)).- Einen Stream kannst du auch aus einem Array machen:
Arrays.stream(prices), und aus einzelnen Werten:Stream.of("Kaffee", "Tee").
Sieh es dir an
Was durch den Filter kommt
List<Integer> prices = List.of(12, 75, 40, 130, 58);List<String> labels = prices.stream() .filter(p -> p >= 50) .map(p -> p + " €") .toList();System.out.println(labels);Schritt für Schritt, in Worten
- Fünf Preise. stream() legt nur den Weg an: Noch ist nichts hindurchgegangen.
- 12 besteht p >= 50 nicht und fällt heraus. Es kommt nie bei map an.
- 75 besteht filter, map macht daraus den Text „75 €“, toList() sammelt ihn.
- Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
- 130 und 58 kommen durch. Die Liste prices ändert sich nicht.
- Das Ergebnis ist eine neue Liste mit drei Strings.
Die fünf Preise laufen einer nach dem anderen über das Förderband. 12 kommt bei filter an und fällt heraus. 75 kommt durch, map macht daraus den Text „75 €“, und er landet in der Liste. Dann fällt 40 heraus, und 130 und 58 kommen durch.
Jedes Element geht den ganzen Weg, bevor das nächste startet. Und nichts startet, bis die terminale Operation toList() ein Ergebnis verlangt.
Genauer, für Neugierige
Das heißt Lazy Evaluation (bedarfsgesteuerte Auswertung). Die Zwischenoperationen merken sich nur, was zu tun ist. Sieh es dir mit einer Ausgabe in jedem Schritt an:
Stream<Integer> stream = List.of(12, 75, 40).stream() .filter(p -> { System.out.println("filter " + p); return p >= 50; }) .map(p -> { System.out.println("map " + p); return p * 2; });System.out.println("Der Stream ist bereit, noch ist nichts hindurchgegangen.");System.out.println(stream.toList());Der Stream ist bereit, noch ist nichts hindurchgegangen.filter 12filter 75map 75filter 40[150]- Die erste Zeile der Ausgabe erscheint, bevor auch nur ein Element durch den Stream gelaufen ist.
map 75kommt direkt nachfilter 75: Die Elemente warten nicht, bis alle durchfiltergelaufen sind.- Deshalb hält
limit(3)das Förderband an, sobald drei Elemente am Ende angekommen sind. Die übrigen werden gar nicht erst geprüft. parallelStream()schickt die Elemente gleichzeitig durch mehrere Threads. Das hilft bei vielen Daten und aufwendigen Berechnungen. Bei kleinen Listen ist es langsamer, und die Lambdas dürfen keine gemeinsamen Daten anfassen. Um Threads geht es im Modul „Programme in der echten Welt“.
Die Idee
Summe, Anzahl, Durchschnitt
Ein Stream kann statt mit einer Liste auch mit einer Zahl enden. Die Summe der geraden Zahlen:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);int sum = numbers.stream().filter(n -> n % 2 == 0).mapToInt(n -> n).sum();System.out.println("Summe der geraden Zahlen: " + sum);Summe der geraden Zahlen: 6mapToInt(n -> n) macht aus einem Stream<Integer> einen Stream von int. Er heißt IntStream und hat sum(), average(), max() und min(). Für double gibt es mapToDouble und DoubleStream.Dasselbe für die Bestellungen:
long big = orders.stream().filter(o -> o.getTotal() >= 50).count();double revenue = orders.stream().mapToDouble(Order::getTotal).sum();double average = orders.stream().mapToDouble(Order::getTotal).average().orElse(0);double maxTotal = orders.stream().mapToDouble(Order::getTotal).max().orElse(0);
System.out.println("Bestellungen ab 50 €: " + big);System.out.printf("Umsatz: %.2f €%n", revenue);System.out.printf("Durchschnitt: %.2f €%n", average);System.out.printf("Größte: %.2f €%n", maxTotal);Bestellungen ab 50 €: 3Umsatz: 312.40 €Durchschnitt: 62.48 €Größte: 120.00 €count()liefert die Anzahl der Elemente, die am Ende angekommen sind. Der Typ istlong.average()undmax()geben nicht direkt eindoublezurück. Gibt es keine Bestellungen, gibt es auch keinen Durchschnitt. Deshalb geben sie „vielleicht eine Zahl“ zurück, undorElse(0)sagt, was gelten soll, wenn es keine gibt. Das ist die nächste Idee.
Die Idee
Sortieren, abschneiden, ohne Wiederholungen
List<Integer> ratings = List.of(5, 3, 5, 4, 2, 4, 5);System.out.println(ratings.stream().distinct().sorted().toList());
List<String> top = orders.stream() .sorted(Comparator.comparing(Order::getTotal).reversed()) .limit(3) .map(Order::getCustomer) .toList();System.out.println(top);
IntStream.range(1, 4).forEach(i -> System.out.println("Tisch " + i));[2, 3, 4, 5][Lukas, Maria, Anna]Tisch 1Tisch 2Tisch 3distinct()entfernt die Wiederholungen. Es vergleicht mitequals.sorted(Comparator.comparing(Order::getTotal).reversed())sortiert ab der größten Bestellung, mit einem Comparator aus der vorigen Lektion.limit(3)lässt nur die ersten drei durch. Zusammen mitsortedist das eine „Top 3“.IntStream.range(1, 4)liefert die Zahlen 1, 2, 3. Das Ende ist nicht dabei, wie beifor (int i = 1; i < 4; i++). MitrangeClosed(1, 4)ist es dabei.
Probier es aus
Was kommt am Ende an?
Was gibt das Programm aus?
List<String> drinks = List.of("kaffee", "tee", "kaffee", "wasser", "tee", "saft");List<String> result = drinks.stream() .distinct() .sorted() .limit(3) .map(String::toUpperCase) .toList();System.out.println(result);distinct() lässt kaffee, tee, wasser, saft übrig. sorted() sortiert sie alphabetisch: kaffee, saft, tee, wasser. limit(3) behält die ersten drei, und map schreibt sie groß.Die Idee
Optional: ein Ergebnis, das fehlen kann
Welche Bestellung ist die größte? Ist die Liste leer, gibt es keine Antwort. Die Methode könnte null zurückgeben, aber dann muss der Aufrufer daran denken, es zu prüfen. Vergisst er es, bricht das Programm irgendwann später mit einer NullPointerException ab.
Deshalb geben max, min und findFirst ein Optional zurück: eine Box, die entweder einen Wert enthält oder leer ist.
Optional<Order> biggest = orders.stream().max(Comparator.comparing(Order::getTotal));biggest.ifPresent(o -> System.out.println("Größte: " + o.getCustomer()));
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();System.out.println(huge.isPresent());System.out.println(huge.map(Order::getCustomer).orElse("keine gefunden"));Größte: Lukasfalsekeine gefundenifPresent(Aktion)führt die Aktion nur aus, wenn es einen Wert gibt.isPresent()sagt dir, ob es einen gibt.orElse(Ersatzwert)liefert den Wert oder den Ersatzwert.map(Funktion)wandelt den Wert um, wenn es einen gibt. Eine leere Box bleibt leer.
Optional hilft auch bei deinen eigenen Methoden, die null zurückgeben. Optional.ofNullable packt in die Box, was es bekommt: einen Wert oder nichts.
static String nickname(String user) { // Gibt den Spitznamen zurück oder null, wenn die Person keinen hat. return user.equals("maria") ? "Mimi" : null;}Optional<String> maria = Optional.ofNullable(nickname("maria"));maria.ifPresent(name -> System.out.println("Spitzname: " + name));
Optional<String> tom = Optional.ofNullable(nickname("tom"));System.out.println(tom.orElse("kein Spitzname"));Spitzname: Mimikein SpitznameGenauer, für Neugierige
get()liefert den Wert, aber wenn die Box leer ist, wirft es eine Exception:
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();System.out.println(huge.get().getCustomer());Exception in thread "main" java.util.NoSuchElementException: No value present at java.base/java.util.Optional.get(Optional.java:143)orElseThrow() macht dasselbe, aber sein Name sagt ehrlich, was passieren wird. Nimm es, wenn eine leere Box ein Fehler ist.
orElse(x)berechnetximmer.orElseGet(() -> ...)ruft die Funktion nur auf, wenn es keinen Wert gibt. Den Unterschied merkst du, wenn der Ersatzwert aufwendig zu berechnen ist.average()vonIntStreamundDoubleStreamgibt einOptionalDoublezurück, undmax()vonIntStreamgibt einOptionalIntzurück. Sie sind dasselbe für primitive Typen:getAsDouble(),orElse(0).Optionalist für das Ergebnis einer Methode da. Benutze es nicht als Feld einer Klasse oder als Parameter.anyMatch,allMatchundnoneMatchantworten mittrueoderfalse, ob irgendein, jedes oder kein Element die Bedingung erfüllt.
Probier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
Ein Stream ist ein Förderband, keine Liste. count() startet ihn, und dabei wird er verbraucht. Der zweite Versuch in Zeile 4 bricht das Programm ab:
Anzahl: 2Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closedBrauchst du zwei Ergebnisse, sammle die gefilterten Preise einmal mit toList() und benutze die Liste: expensive.size() und expensive. Oder mach für jedes Ergebnis mit prices.stream() einen neuen Stream.
Die Idee
Gruppieren und verbinden
Wie viele Bestellungen gibt es aus jeder Stadt, und wie hoch ist der Umsatz?
Map<String, Long> countByCity = orders.stream() .collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.counting()));System.out.println(countByCity);
Map<String, Double> revenueByCity = orders.stream() .collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.summingDouble(Order::getTotal)));revenueByCity.forEach((city, sum) -> System.out.printf("%s: %.2f €%n", city, sum));
String customers = orders.stream() .map(Order::getCustomer) .sorted() .collect(Collectors.joining(", "));System.out.println(customers);{Berlin=1, Dresden=2, Hamburg=2}Berlin: 120.00 €Dresden: 76.90 €Hamburg: 115.50 €Anna, Laura, Lukas, Maria, Tomcollect sammelt den Stream nach einer Regel aus der Klasse Collectors:
groupingBy(Order::getCity, TreeMap::new, Collectors.counting()): Gruppiere nach Stadt, lege die Gruppen in eineTreeMap, zähle jede Gruppe.summingDouble(Order::getTotal)stattcounting()addiert, statt zu zählen.joining(", ")verbindet die Strings zu einem, mit einem Komma dazwischen.
TreeMap::new ist eine Konstruktorreferenz. Sie sagt, welche Art von Map groupingBy machen soll. Ohne sie ist das Ergebnis eine HashMap, und die Reihenfolge der Städte darin ist nicht garantiert.
Genauer, für Neugierige
- Nur mit der Funktion,
groupingBy(Order::getCity), ist das Ergebnis eineMap<String, List<Order>>: für jede Stadt eine Liste ihrer Bestellungen. - Willst du statt der Bestellungen eine Liste von Namen, wandelt
mappingjedes Element um, bevor es in die Gruppe kommt:
Map<String, List<String>> namesByCity = orders.stream() .collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.mapping(Order::getCustomer, Collectors.toList())));System.out.println(namesByCity);{Berlin=[Lukas], Dresden=[Tom, Anna], Hamburg=[Maria, Laura]}Collectors.partitioningBy(Bedingung)teilt in zwei Gruppen:trueundfalse.Collectors.toMap(Schlüssel, Wert)macht eine Map mit einem Wert pro Schlüssel und wirft eine Exception, wenn sich ein Schlüssel wiederholt.
Übung
Berichte mit Streams
java.util.stream.Collectors und für Arrays java.util.Arrays.1. Verkäufe nach Kategorie. Eine Klasse Sale mit Produkt, Kategorie, Menge und Stückpreis und einer Methode getRevenue() (Menge mal Preis). Addiere den Umsatz pro Kategorie in einer TreeMap und gib den Gesamtumsatz aus. Die Verkäufe: Kaffeebohnen (Kategorie „Kaffee und Tee“), 12 Stück zu 18,50 €; Tee („Kaffee und Tee“), 30 zu 4,20 €; Schokolade („Snacks“), 40 zu 2,80 €; Kekse („Snacks“), 25 zu 3,10 €; Honig („Feinkost“), 8 zu 12 €; noch einmal Kaffeebohnen, 5 zu 18,50 €.
Feinkost: 96.00 €Kaffee und Tee: 440.50 €Snacks: 189.50 €Gesamt: 726.00 €2. Die Top 3 der Produkte. Finde mit denselben Verkäufen die drei Produkte mit dem höchsten Umsatz. Der Kaffee steht in zwei Verkäufen, deshalb gruppierst du zuerst nach Produkt und addierst, dann sortierst du die Einträge der Map nach dem Wert. Tipp: map.entrySet().stream() und Map.Entry.<String, Double>comparingByValue().reversed().
Kaffeebohnen: 314.50 €Tee: 126.00 €Schokolade: 112.00 €3. Die Durchschnittsnote. Lies eine Zeile mit Noten, getrennt durch Leerzeichen. Gib den Durchschnitt aus, die höchste Note und wie viele ausgezeichnet sind (5,50 und mehr). Tipp: Arrays.stream(line.split(" ")).mapToDouble(Double::parseDouble).
4.50 5.75 6.00 3.00 5.50Durchschnitt: 4.95Höchste: 6.00Ausgezeichnet: 34. Die Wörter in einem Text. Lies eine Zeile Text und gib die Wörter darin ohne Wiederholungen aus, alphabetisch, kleingeschrieben, durch Kommas getrennt. Teile an allem, was kein Buchstabe ist: split("[^\\p{L}]+").
Kaffee mit Milch, Kaffee ohne Zucker. Tee mit Honig!honig, kaffee, milch, mit, ohne, tee, zuckerWeitere Aufgaben
- Das längste Wort. Finde mit
max(Comparator.comparing(String::length))das längste Wort in einem Satz. Was gibt das Programm bei einer leeren Zeile zurück? - Durchschnitt pro Klasse. Schüler mit Name, Klasse und Note: die Durchschnittsnote jeder Klasse mit
groupingByundCollectors.averagingDouble. - Große und kleine Bestellungen. Prüfe mit
anyMatch, ob es eine Bestellung über 1000 € gibt, und mitallMatch, ob alle mindestens 10 € betragen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen · HashMap und HashSet · Klassen in Klassen · Lambdas
- Geht weiter in: record, switch und Muster · Tests mit JUnit · Datenbanken · Reflection und Annotationen
- Aufgaben zum Thema: Modernes Java