17.6 Intermediäre Operationen
Alle terminalen Operationen führen zu keinem neuen veränderten Stream, sondern beenden den aktuellen Stream. Anders sieht das bei intermediären Operationen aus: Sie verändern den Stream, indem sie etwa Elemente herausnehmen, auf andere Werte und Typen abbilden oder sortieren. Jede der intermediären Methoden liefert ein neues Stream-Objekt. Das haben wir in den ersten Schreibweisen durch die Konkatenation etwas verschleiert, aber vollständig sieht es so aus:
Vollständige Schreibweise | Kaskadierte Schreibweise |
---|---|
Stream<Object> a = | Stream |
[»] Hinweis
Keine Strommethode darf die Stromquelle modifizieren, da das Ergebnis sonst unbestimmt ist. Veränderungen treten nur entlang der Kette von einem Strom zum nächsten auf. Wir haben das schon in Abschnitt 17.5.8, »Ergebnisse in einen Container schreiben, Teil 2: Collector und Collectors«, diskutiert.
17.6.1 Element-Vorschau
Eine einfache intermediäre Operation ist peek(…). Sie darf sich das aktuelle Element während des Durchlaufs anschauen.
interface java.util.stream.Stream<T>
extends BaseStream<T,Stream<T>>
Stream<T> peek(Consumer<? super T> action)
[zB] Beispiel
Schaue in den Strom vor und nach einer Sortierung:
System.out.println( Stream.of( 9, 4, 3 )
.peek( System.out::println ) // 9 4 3
.sorted()
.peek( System.out::println ) // 3 4 9
.collect( Collectors.toList() ) );
17.6.2 Filtern von Elementen
Eine der wichtigsten Stream-Methoden ist filter(…): Sie liefert alle Elemente im Stream, die einem Kriterium genügen, und ignoriert alle anderen, die nicht diesem Kriterium genügen.
interface java.util.stream.Stream<T>
extends BaseStream<T,Stream<T>>
Stream<T> filter(Predicate<? super T> predicate)
[zB] Beispiel
Gib alle Wörter aus, bei denen zwei beliebige Vokale hintereinander vorkommen:
Stream.of( "Moor", "Aha", "Meister" )
.filter( Pattern.compile( "[aeiou]{2}" ).asPredicate() )
.forEach( System.out::println ); // Moor Meister
17.6.3 Statusbehaftete intermediäre Operationen
Die allermeisten intermediären Operationen können Elemente direkt während des Durchlaufs bewerten und verändern, sodass keine speicherintensive Zwischenspeicherung nötig ist. Einige intermediäre Operationen haben jedoch einen Status. Dazu zählen:
limit(long): begrenzt den Strom auf eine gewisse Anzahl maximaler Elemente.
skip(long): überspringt eine Anzahl Elemente.
distinct(): löscht alle doppelten Elemente.
sorted(…): sortiert den Strom.
Stream<T> limit(long maxSize)
Stream<T> skip(long n)
Stream<T> distinct()
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
[zB] Beispiel 1
Was ist das längste Wort der Liste?
String longest = Stream.of( "Autos", "können", "nicht", "versaut", "genug", "sein" )
.sorted( Comparator.comparing( String::length ).reversed() )
.findFirst().get();
System.out.println( longest ); // versaut
Alternativ mit max(…):
String longest = …
.max( Comparator.comparing( String::length ) )
.get();
[zB] Beispiel 2
Zerlege eine Zeichenkette in Teilzeichenketten, bilde diese auf das ersten Zeichen ab, und lösche aus dem Stream doppelte Elemente:
List<String> list = Pattern.compile( " " ).splitAsStream( "Pu Po Aha La" )
.map( s -> s.substring(0, 1) )
.peek( System.out::println ) // P P A L
.distinct() // \/\/\/
.peek( System.out::println ) // P A L
.collect( Collectors.toList() );
System.out.print( list ); // [P, A, L]
peek(…) macht gut deutlich, wie die Elemente vor und nach dem Anwenden von distinct() aussehen.
[»] Hinweis
Eine Methode wie skip(…) sieht auf den ersten Blick unschuldig aus, kann aber ganz schön auf den Speicherbedarf und die Performance gehen, wenn parallele Ströme mit Ordnung (etwa durch Sortierung) ins Spiel kommen. Bei einem stream.parallel().sorted().skip(10000) müssen trotzdem alle Elemente erst sortiert werden, damit die ersten 10.000 übersprungen werden können. Sequenzielle Ströme bzw. Ströme ohne Ordnung (die liefert die Stream-Methode unordered()) sind ungleich schneller, aber natürlich nicht in jedem Fall möglich.
17.6.4 Präfix-Operation
Unter einem Präfix verstehen wir eine Teilfolge eines Streams, die beim ersten Element beginnt. Wir können mit limit(long) selbst ein Präfix generieren, doch im Allgemeinen geht es darum, eine Bedingung zu haben, die alle Elemente eines Präfix-Streams erfüllen müssen, und den Stream zu beenden, wenn die Bedingung für ein Element nicht mehr gilt. Java 9 deklariert dafür zwei neue Methoden, takeWhile(…) und dropWhile(…):
default Stream<T> takeWhile(Predicate<? super T> predicate)
default Stream<T> dropWhile(Predicate<? super T> predicate)
Die deutsche Übersetzung von takeWhile(…) wäre »nimm, solange predicate gilt« und dropWhile(…) »lass fallen, solange predicate gilt«.
[zB] Beispiel
Der Stream soll beim Eintreffen des Wortes »Trump« sofort enden:
new Scanner( "Dann twitterte Trump am 7. Nov. 2012: "
+ "'It's freezing and snowing in New York--we need global warming!'" )
.useDelimiter( "\\P{Alpha}+" ).tokens()
.takeWhile( s -> !s.equalsIgnoreCase( "trump" ) )
.forEach( System.out::println ); // Dann twitterte
}
Der Stream soll nach dem längsten Präfix beginnen, und dann enden, wenn eine Zahl kleiner gleich 0 ist:
Stream.of( 1, 2, -1, 3, 4, -1, 5, 6 )
.dropWhile( i -> i > 0 ) // !(i>0), also i<=0 wird übersprungen
.forEach( System.out::println ); // -1 3 4 -1 5 6
Das Element, das das Prädikat erfüllt, ist selbst das erste Element im neuen Stream. Wir können es mit skip(1) überspringen.
Erfüllt schon bei takeWhile(…) das erste Element nicht das Prädikat, so ist der Stream leer. takeWhile(…) und dropWhile(…) können zusammen verwendet werden: So liefert Stream.of( 1, 2, -1, 3, 4, -1, 5, 6 ).dropWhile( i -> i > 0 ).skip( 1 ).takeWhile( i -> i > 0 ).forEach( System.out::println ); die Ausgaben 3 4.
[»] Hinweis
Präfixe sind nur für geordnete Streams sinnvoll. Und wenn Streams parallel sind, müssen sie für die Präfixberechnung wieder in die richtige Reihenfolge gebracht werden. Das ist eine eher teure Operation.
17.6.5 Abbildungen
Die Fähigkeit, Elemente im Strom auf neue Elemente abzubilden ist eines der mächtigsten Werkzeuge der Stream-API. Zu unterscheiden sind drei Typen von Abbildungsmethoden:
Die einfachste Variante ist map(Function). Die Funktion bekommt das Stromelement als Argument und liefert ein Ergebnis, das dann in den Strom kommt. Hierbei kann sich der Typ ändern, wenn die Funktion nicht den gleichen Typ gibt, wie sie nimmt. Die Funktion wird nach und nach auf jedem Element angewendet, und die Reihenfolge ergibt sich aus der Ordnung des Stroms.
Die drei Methoden mapTo[Int|Long|Double]([int|long|double]Function) arbeiten wie map(Function), nur liefern die Funktionen einen primitiven Wert, und das Ergebnis ist ein primitiver Stream.
flatMapXXX(XXXFunction)-Methoden ersetzen ebenfalls Elemente im Stream, mit dem Unterschied, dass die Funktionen alle selbst Stream-Objekte liefern, deren Elemente dann in den Ergebnisstrom gesetzt werden. Der Begriff flat (engl. für »flach«) kommt daher, dass die »inneren« Streams nicht selbst alle als Elemente in den Ursprungs-Stream kommen (sozusagen ein Stream<Stream>), sondern »flachgeklopft«werden. Anwendungsbeispiele sind in der Regel Szenarien wie »n Spieler assoziieren m Gegenstände, gesucht ist ein Strom aller Gegenstände aller Spieler«. Die Funktion kann null liefern, wenn nichts in den Stream gelegt werden soll.
interface java.util.stream.Stream<T>
extends BaseStream<T,Stream<T>>
<R> Stream<R> map(Function<? super T,? extends R> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
IntStream flatMapToInt(Function<? super T,? extends IntStream> mapper)
LongStream flatMapToLong(Function<? super T,? extends LongStream> mapper)
DoubleStream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)
[zB] Beispiel
Konvertiere ein String-Array mit Zahlen in ein long-Array:
String[] numbers = { "1", "2", "3" };
long[] parseInts = Stream.of( numbers ).mapToLong( Long::parseLong ).toArray();
[zB] Beispiel
Die Methode Locale.getAvailableLocales() liefert ein Array von Locale-Objekten, die ein System unterstützt. Wir interessieren uns für alle Ländercodes:
Stream.of( Locale.getAvailableLocales() )
.map( Locale::getCountry )
.distinct()
.forEach( System.out::println );
Über ein Locale-Objekt erfragt DateFormatSymbols.getInstance(locale).getWeekdays() die Namen der Wochentage. Wir interessieren uns als Ergebnis für alle Wochentage aller installierten Gebiete:
Stream.of( Locale.getAvailableLocales() )
.flatMap( l -> Stream.of( DateFormatSymbols.getInstance( l ).getWeekdays() ) )
.filter( s -> ! s.isEmpty() )
.distinct()
.forEach( System.out::println );
[+] Tipp
Die an flatMap(…) übergebene Funktion muss als Ergebnis einen Stream liefern oder null, wenn nichts passieren soll. Es ist nicht verkehrt, immer einen Stream zu liefern und statt null der Funktion einen leeren Stream mit Stream.empty() zurückgeben zu lassen. Anwendungen für diese leeren Ströme kommen aus folgendem Szenario: In der API gibt es Methoden, die statt Arrays bzw. Sammlungen nur null zurückgeben. Nehmen wir an, result ist so eine Rückgabe, die null oder ein Array sein kann, dann bekommen wir einen Stream wie folgt:
Stream<…> flatStream = Optional.ofNullable( result )
.map( Stream::of ).orElse( Stream.empty() );
Die Fallunterscheidung, ob die Sammlung null ist, ist hier funktional mit Optional gelöst, alternativ mit result != null ? result : Stream.empty(). Das statische Stream.of(…), das wir hier über eine Methodenreferenz nutzen, funktioniert für ein Array; für einen Stream aus einer Sammlung wäre result.stream() nötig. Fassen wir das in einem Beispiel zusammen. Die Class-Methode getEnumConstants()liefert ein Array von Konstanten, wenn das Class-Objekt eine Aufzählung repräsentiert – andernfalls ist das Array nicht etwa leer, sondern null. (So lässt sich unterscheiden, ob das Class-Objekt entweder keine Elemente hat oder überhaupt kein Aufzählungstyp ist.) Wir wollen von drei Class-Objekten alle Konstanten einsammeln und ausgeben:
Stream.of( Object.class, Thread.State.class, DayOfWeek.class )
.flatMap( clazz -> Optional.ofNullable( clazz.getEnumConstants() )
.map( Stream::of ).orElse( Stream.empty() ) )
.forEach( System.out::println );
null ist oft ein ungebetener Gast, und die Stream-Methode ofNullable(…) in Java 9 hilft, Fehler zu vermeiden. Nehmen wir Folgendes, um alle Koordinaten in einen Stream zu bekommen:
Stream.of( null, new Point( 1, 2 ), null, new Point( 3, 4 ) )
.flatMap( q -> Stream.of( q.x, q.y ) )
.forEach( System.out::println );
Bei flatMap(…) wird eine NullPointerException folgen. Mit filter(…) könnten wir im Vorfeld jede null ausblenden, doch eine andere Lösung wäre:
Stream.of( null, new Point( 1, 2 ), null, new Point( 3, 4 ) )
.flatMap( p -> Stream.ofNullable( p ).flatMap( q -> Stream.of( q.x, q.y ) ) )
.forEach( System.out::println );