Overview:
Im letzten Tutorial haben wir uns angesehen, was Streams sind und wie diese funktionieren. Jetzt wollen wir uns die einzelnen Methoden für Streams genauer anschauen. Man unterscheidet zwischen Terminal Operations und Intermediate Operations.
Terminal Operations:
Intermediate Operations:
Terminal Operations leifern keinen Stream als Rückgabewert. Da Streams nur einmal verwendet werden können, ist der Stream nach einer terminalen Operation nicht mehr verfügbar und die Kette bricht ab.
Es gibt eine ganze Reihe von super nützlichen Terminal Operations, die wir uns jetzt einmal im Detail ansehen wollen.
Diese Operation führt eine Aktion für jedes Element im Stream aus:
stream.forEach(System.out::print);
Die count() Methode zählt die Anzahl der Elemente eines Streams:
List<String> list = Arrays.asList("Wolf", "Tiger", "Drache");
Stream<String> stream = list.stream();
System.out.println(stream.count()); // 3
Die Methoden min() und max() nehmen einen benutzerdefinierten Comparator auf und finden das kleinste bzw. größte Element in einem Stream (gemäß der Sortierordnung des Comparators). Wichtig zu wissen: Die Methoden geben keinen konkreten Wert, sondern ein Objekt vom Typ Optional zurück.
List<String> list = Arrays.asList("*", "**", "***");
Stream<String> stream = list.stream();
Optional min = stream.min((s1,s2)->s1.length()-s2.length());
System.out.println(min); // Optional[*]
Die Methode findFirst() liefert das erste Element des Streams (sofern dieser nicht leer ist):
Predicate<Player> filter = player -> player.getName().equals("Elon");
Optional<Player> playerFound = spieler.stream().filter(filter).findFirst();
playerFound.ifPresent(System.out::println); // Elon
In diesem Beispiel definieren wir zuerst einen Filter vom Typ Predicate<Player>. Im nächsten Schritt erzeugen wir aus einer Liste einen Stream, setzen den Filter darauf an und lassen uns dann das erste gefundene Element mit findFirst() liefern.
Der Rückgabetyp von findFirst() ist Optional<T>. Hier ist vorsicht geboten: Sollte die filter() Methode keinen Treffer landen, gibt es also kein Element, kann findFirst() auch kein erstes Element liefern. Da Optional<T> nicht null sein darf, könnte ein weiterer Methodenaufruf auf der Referenz playerFound eine Runtime-Exception auslösen.
Mit der nützlichen Optional<T>-Methode isPresent() können wir aber easy prüfen, ob die Referenz null ist oder auf ein Objekt verweist:
boolean check = playerFound.isPresent() ? true : false;
System.out.println(check); // true
findAny() liefert ein beliebiges Element aus dem Stream zurück. Von der Handhabung funktioniert findAny() wie findFirst().
Doch es gibt einen gewichtigen Unterschied, der erst mit beim Einsatz von parallelen Streams relevant wird: findAny() liefert Java nämlich das erste Element zurück, dass es "greifen" kann, anstatt wie bei findFirst(), das erste Element im Stream zurückzugeben.
Der Java-Entwickler Lino hat den Vorteil von findAny() in einem paralellen Stream präzise so formuliert:
Take a stream of two elements and process them to get a result. The first takes 400 years to complete. The second 1 minute. findFirst will wait the whole 400 years to return the first elements result. While findAny will return the second result after 1 minute. See the difference? Depending on the circumstances of what exactly you're doing, you just want the fastest result, and don't care about the order.
Die Methoden allMatch(), anyMatch() und noneMatch() durchsuchen einen Stream und liefern einen booleschen Wert, ob der geprüfte Stream die Bedingung eines Predicate<T> erfüllt. Oftmals werden diese Methoden genutzt, um eine Liste nach dem Vorkommen einer bestimmten Prüfbedingung zu durchsuchen.
List<String> liste = Arrays.asList("Blood", "Steel", "Forgive");
Predicate<String> pred = p -> p.startsWith("S");
System.out.println(liste.stream().anyMatch(pred)); // true
System.out.println(liste.stream().allMatch(pred)); // false
System.out.println(liste.stream().noneMatch(pred)); // false
Wie du siehst, haben wir dreimal einen Stream erstellt. Das ist kein "bad code", sondern bei diesen drei Methoden einfach notwendig. Denn wir können zwar das definierte Predicate immer wieder verwenden, aber wir benötigen jedes mal einen neuen Stream.
Versuchen wir es trotzdem, erhalten wir eine RuntimeException:
Stream<String> stream = Arrays.asList("Blood", "Steel", "Forgive").stream();
Predicate<String> pred = p -> p.startsWith("S");
System.out.println(stream.anyMatch(pred)); // true
System.out.println(stream.allMatch(pred)); // RUNTIME EXCEPTION !!
Die reduce()-Methode reduziert einen Stream zu einem einzelnen Objekt, indem die Elemente gleichen Typs miteinander kombiniert werden.
Hier ein Beispiel, um die Elemente einem Stream<String> zu einem einzelnen String zu reduzieren:
Stream<String> stream = Stream.of("M", "A", "R", "S");
String word = stream.reduce("",(a,b) -> a+b);
System.out.println(word); // MARS
Wie du siehst, gibt es einen Initialwert (leerer String: ""). Auf diesen akkumulieren wir die einzelnen String-Elemente des Streams.
Eine andere Reduktion wäre etwa das Multiplizieren aller Zahlen eines Integer-Streams zu einem einzigen Integer-Wert:
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
Integer number = stream.reduce(1, (a,b) -> a * b);
System.out.println(number); // 24
Der Initialwert stellt sicher, dass ein Wert des angegebenn Datentyps zurückgeliefert wird. Wenn wir keinen Initialwert angeben, wird ein Optional zurückgegeben, denn es könnte ja sein, dass keine Daten vorhanden sind.
Ganz oft ist es nötig, einen Stream zum Abschluss in einer Collection<E> zu speichern. Dazu gib es es die super praktische Methode Stream.collect(), die wir auf einer Stream-Instanz anwenden können. Die Methode liest dann die Daten aus dem Stream und liefert daraus ein List-Objekt zurück.
Bereits vordefinierte Implementierungen für collect können wir in der Utility-Klasse java.util.stream.Collectors finden.
Sehen wir uns das Ganze aber doch einmal konkret an. Gegeben ist ein Stream:
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4).stream();
Auf dem Stream können wir jetzt die collect-Methode mit der speziellen Implementierung Collectors.toList() ausführen, um eine Collection vom Typ List zu erhalten:
List<Integer> liste = stream.collect(Collectors.toList());
Mit der Implementierung Collectors.toList() bekommen wir, wie gesagt, ein Sammlung vom Typ List zurück. Für mehr Kontrolle über den Rückgabegabetyp der erzeugten Collection können wir für die collect-Methode alternativ die Implementierung Collectors.toCollection() verwenden.
Für den Fall, dass wir beispielsweise eine ArrayList brauchen, sieht das dann so aus:
List<Integer> liste = stream.collect(Collectors.toCollection((ArrayList::new)));
Intermediate Operationens liefern als Rückgabewert wieder einen Stream, auf dem man wieder eine Stream-Operation ausführen kann. Somit lassen sich ganze Operationen-Ketten in der Pipeline durchführen ("Chaining").
Die nützlichsten stellen wir euch hier im Detail vor:
Die filter()-Methode liefert einen gefilterten Stream. Nach welchem Kriterium gefiltert werden soll, legt der Programmierer mit einem Predicate<T> fest, das der filter()-Methode übergeben wird.
Hier ein Beispiel, um aus einer ArrayList einen Stream zu erstellen, der gefilert ist nach Personen über 30 Jahre:
List<Person> personen = new ArrayList<>();
Person person1 = new Person("Riker", 30);
Person person2 = new Person("Stargazer", 40);
Person person3 = new Person("Moritz", 50);
personen.add(person1);
personen.add(person2);
personen.add(person3);
Predicate<Person> mehrAls50 = person -> person.getAge() > 30;
Stream<Person> stream = personen.stream().filter(mehrAls50);
stream.forEach(System.out::println);
Raus kommt dann:
Stargazer
Moritz
Alternativ können wir statt dem Lambda auch eine Methodenreferenz verwenden, die auf die Methode over30() in der Klasse Person verweist:
Stream<Person> stream = personen.stream().filter(Person::over30);
Mehrstufige Filterung
Eine wichtigte Eigenschaft von Intermediate Operations ist die Möglichkeit, dass man diese Verketten kann. Durch das "Chaining" erhalten wir eine Pipeline, mit der wir etwa mehrstufig filtern können:
Stream<Person> stream = personen.stream().filter(Person::over30).
filter(person -> person.getName().equals("Stargazer")).
filter(person -> person.getPunkte() > 1);
Die disctinct()-Methode entfernt doppelte Werte und liefert einen von Duplikaten bereinigten Stream zurück. Was ein Duplikat ist, wird von Java durch den Aufruf von equals() bestimmt. Hier ist ein Beispiel:
Stream<String> stream = Stream.of("A", "A", "B", "C", "C");
stream.distinct().forEach(System.out::print); // ABC
Die map() Methode von Streams ermöglicht es, Daten zu extrahieren und in einen anderen Datentyp zu überführen.
Wenn wir beispielsweise aus einem Stream vom Typ Person alle Namen (Strings) extrahieren wollen und diese dann in einem neuen Stream vom Datentyp String speichern wollen, ist map() perfekt dafür geeignet. Hier ein konkretes Beispiel:
List<Person> personen = new ArrayList<>();
Person person1 = new Person("Riker", 30);
Person person2 = new Person("Stargazer", 40);
Person person3 = new Person("Moritz", 50);
personen.add(person1);
personen.add(person2);
personen.add(person3);
Stream<String> names = personen.stream().map(person -> person.getName());
names.forEach(System.out::println); // Riker Stargazer Moritz
Die Methode sorted() gibt einen Stream mit sortierten Elemeten zurück. Standardmäßig nutzt Java für sorted() die natürliche Sortierordnung:
Stream<Integer> s1 = Stream.of(3, 0, 1, 2);
s1.sorted().forEach(System.out::print); // 0123
Stream<String> s2 = Stream.of("Banane, Apfel, Zitrone, Kiwi");
s2.sorted().forEach(System.out::print); // Banane, Apfel, Zitrone, Kiwi
Keine Sorge: Der zweite Stream s2 wurde richtig sortiert. In Java werden Strings nämlich in lexikographischer Ordnung sortiert.
Wenn wir einen Comparator festlegen, können wir auch eine eigene Ordnung festlegen. Hier ein Beispiel:
List<Player> sortiert = player.stream()
.sorted(Comparator.comparingInt(Player::getPunkte))
.collect(Collectors.toList());
In diesem Beispiel haben wir der Methode sorted() einen Comparator implementiert als Methodenreferenz übergeben, der einen Stream von Player-Objekten aufsteigend nach dem Wert des Attributs punkte sortiert. Am Ende der Pipeline geben wir den Stream als <List>Player wieder zurück.
Wir erinnern uns: Einen Stream können wir nur einmal verwenden:
Stream<Player> stream = player.stream().filter(Player::over30);
stream.forEach(System.out::println);
Stream<Player> streamV2 = stream.filter(p -> p.getAge() > 30);
streamV2.forEach(System.out::println);
Mit dem Versuche, den Stream wiederzuverwenden, erzeugen wir eine IllegalStateException. Der Grund ist klar: stream has already been operated upon or closed - Wie gesagt: ein stream kann Daten nur einmal bereitstellen.
Um dieses Dilemma zu lösen und einen Stream wiederverwendbar zu machen, wurde die Methode peek() erfunden. Diese Methode ist ´ besonders nützlich für das Debugging, da sie uns erlaubt, ein "Zwischenergebnis" einer Pipeline als Stream zu erhalten und mit diesem dann weiterzuarbeiten - und so sieht das dann aus:
Player p1 = new Player("Andi", 25);
Player p2 = new Player("Ben", 33);
Player p3 = new Player("Chorus", 40);
List<Player> player = new ArrayList<>();
player.add(p1);
player.add(p2);
player.add(p3);
// Stream erstelellen
Stream<Player> stream = player.stream().filter(Player::over30);
// peek(), um den Zwischenstand zu überprüfen
Stream<Player> streamPeek = stream.peek(System.out::println); // Ben33Chorus40
// Weiterarbeiten
Stream<Player> streamV2 = streamPeek.filter(p -> p.getName().equals("Ben"));
streamV2.forEach(System.out::println); // Ben40
Die peek()-Methode ist die letzte Intermediate Operation, die wir uns ansehen. Besonders nützlich ist sie fürs Debugging, da sie uns erlaubt, ein "Zwischenergebnis" einer Pipeline zu erhalten und danach mit dem Stream weiterzuarbeiten.
Java Basics
[Java einrichten] [Variablen] [Primitive Datentypen] [Operatoren] [if else] [switch-case] [Arrays] [Schleifen]
Objektorientierung
[Einstieg] [Variablen ] [Konstruktor] [Methoden] [Rekursion] [Statische Member] [Initializer] [Pass-by-value] [Objektsammlungen] [Objektinteraktion] [Objekte löschen]
Klassenbibliothek
[Allgemeines] [String ] [Math] [Wrapper] [Scanner] [java.util.Arrays] [Date-Time-API]
Vererbung
[Einstieg Vererbung] [Konstruktoren bei Vererbung ] [Der protected Zugriffsmodifikator] [Abstrakte Klassen und Methoden] [Polymorphie in Java] [Typumwandlung] [Die Klasse Object] [Die toString()-Methode] [Objekte vergleichen] [Was ist ein Interface?]