Streams erlauben die Ausführung von Operationen auf Arrays und Listen.v.8.0
Streams des Interface java.util.stream.Stream<T>
, nicht zu verwechseln mit den Ein- und Ausgabe-Streams des
Packages java.io
, stellen Ströme von
Referenzen dar, die es erlauben, verkettete Operationen auf
diesen Referenzen nacheinander oder parallel auszuführen.
Die Daten, die durch die Referenzen repräsentiert werden,
werden durch den Stream selbst nicht verändert.
Das
Interface und die von ihm abgeleiteten Interfaces stellen
lediglich eine Vielzahl von Methoden bereit, die in zwei
Hauptkategorien eingeteilt werden und meist Lambda
Ausdrücke als Argumente übergeben bekommen:
- intermediäre Operationen (intermediate operations)
liefern wiederum einen
Stream
, der weiterverarbeitet werden kann (z.B.filter(), map(), distinct(), sorted()
, etc.). - terminale Operationen (terminal operations)
führen ihrerseits Operationen auf den Referenzen des
Streams aus (
forEach(), reduce(), toArray()
, etc.). Sie können einen Wert liefern und beenden den Strom. Ist ein Strom einmal geschlossen, so können keine weiteren Operationen auf ihm ausgeführt werden.
Streams erzeugen
Streams können aus Arrays, Listen, anderen Collections und
aus Einzelobjekten, sowie mittels sog. StreamBuilder
erzeugt werden. Je nach verwendeter Methode kann das Ergebnis
jedoch unterschiedlich ausfallen.
Erzeugung aus Arrays
Stream.of(myArray) Arrays.stream(myArray)
Beide Varianten liefern jeweils einen Stream
, sind
jedoch nicht gleich. Dies zeigt das folgende Beispiel anhand
eines Arrays mit primitiven int
-Werten:
int[] nums = {1,2,3,4,5}; Stream.of(nums).forEach(n -> System.out.println(n)); Arrays.stream(nums).forEach(n -> System.out.println(n));
Die Ausgabe sieht wie folgt aus:
[I@5674cd4d 1 2 3 4 5
Im ersten Fall wird hier ein Stream
mit einem
einzigen Wert, nämlich dem Array selbst erzeugt. Nur im
zweiten Fall wird das erzeugt, was wohl in den meisten
Fällen gewünscht sein wird: ein IntStream
aus den einzelnen primitiven Werten des Arrays. Im Gegensatz zum
Interface Stream
besitzt ein IntStream
mit sum(), average(), count()
, etc. Methoden zur
Weiterverarbeitung primitiver int
-Werte.
Erzeugung aus Listen und Sets
Stack<Integer> stack = new Stack<Integer>(); stack.push(32); stack.push(1024); stack.push(8); stack.push(127); stack.stream().sorted().forEach(n -> System.out.print(n + ", ")); System.out.println(); for (int i : stack) { System.out.print(i + ", "); }
Die Ausgabe sieht wie folgt aus:
8, 32, 127, 1024, 32, 1024, 8, 127,
Das Beispiel zeigt zweierlei: Zum einen wird hier aus einem Stack
, einer Erweiterung von java.util.AbstractList
,
durch die Methode stream()
ein Stream
erzeugt, der dann sortiert und dessen Werte schließlich auf
die Konsole ausgegeben werden.
Zum anderen wird am Ende
demonstriert, dass die Sortierung des Streams auf den
eigentlichen Stack
keinen Einfluss hat, da die
ursprüngliche Reihenfolge ausgegeben wird. Wie bereits oben
erwähnt, lässt sich das verallgemeinern: Die Erzeugung
und Abarbeitung eines Streams hat keinerlei Einfluss auf die
zugrunde liegende Datenstruktur!
Erzeugung aus Einzel-Objekten
Stream.of("Ene", "mene", "muh", "und", "raus", "bist", "du").limit(3).forEach(System.out::println);
Die statische Methode of()
des Interface Stream
erzeugt hier einen Stream
aus sieben String
-Objekten. Er wird dann, von vorne beginnend, auf 3 Elemente
beschnitten und schließlich ausgegeben:
Ene mene muh
Im letzten Teil des Ausdrucks wird eine Methodenreferenz verwendet. Die
Methode println()
ist ohne Klammern notiert und
wird vom PrintStream System.out
durch zwei
Doppelpunkte abgetrennt. Dies ist möglich, da die
überladene Methode jeweils nur einen Parameter erwartet,
der hier durch den Stream
geliefert wird. Die
beiden Codezeilen führen zur gleichen Ausgabe:
Stream.of("Ene", "mene", "muh", "und", "raus", "bist", "du").limit(3).forEach(i->System.out.println(i)); Stream.of("Ene", "mene", "muh", "und", "raus", "bist", "du").limit(3).forEach(System.out::println);
Streams können auch direkt, ohne vorherige Angabe von
Literalen, generiert werden. Die Methode iterate()
erzeugt einen unendlichen Stream fortlaufender Zahlen, hier
beginnend bei 0.
Stream.iterate(0,i->i+1).forEach(System.out::println);
Hier muss der Entwickler dafür sorgen, dass dieser
entsprechend begrenzt wird, z.B. durch Angabe von limit()
:
Stream.iterate(0,i->i+1).limit(10).forEach(System.out::println);
Methodenausführung auf Streams
Intermediäre und terminale Operationen
Wie oben erwähnt und in den bisherigen Beispielen bereits
demonstriert, ermöglichen es intermediäre
Operationen, auf einem Stream
Operationen
auszuführen, die wiederum einen Stream
liefern. Darauf wiederum ausgeführt, wird ein weiteres Mal
ein Stream
ausgegeben etc. Auf diese Weise kann in
einer solchen Pipeline ein ursprüngliches Array
oder eine Liste schrittweise immer genauer spezifiziert
gefiltert werden, bis auf dem Endergebnis schließlich eine
terminale Operation ausgeführt wird, die
gleichzeitig den Stream
schließt. Das folgende
Beispiel demonstriert dies anhand eines Integer
-Arrays der Länge 100, das initial mit Pseudo-Zufallswerten
zwischen 0 und 99 belegt wird.
Integer[] i = new Integer[100]; for(int num = 0; num < i.length; num++){ i[num] = new Random().nextInt(100); } int ergebnis = Stream.of(i).mapToInt(k -> k.intValue()).filter(k -> k%3==0).limit(10).sum(); System.out.println(ergebnis);
Das Integer
-Array wird als Argument an die Methode
Stream.of()
übergeben, die einen Stream
vom Typ java.util.stream.Stream
erzeugt. Dieser
wird im nächsten Schritt durch die Methode mapToInt()
in einen Stream
vom Typ java.util.stream.IntStream
gewandelt. Dies geschieht dadurch, dass der Methode ein
Lambda-Ausdruck als Parameter übergeben wird, durch den
alle Integer
-Objekte zu primitiven int
-Werten
gewandelt werden.
Auf diesem IntStream
wird die
Methode filter()
ausgeführt, die es
ermöglicht, jeden Wert des Streams auf eine boolsche
Bedingung hin zu prüfen. Im vorliegenden Fall werden alle
Werte selektiert, die ganzzahlig durch 3 teilbar sind und in den
schließlich zurück gegebenen IntStream
eingetragen.
Die darauf folgende Methode limit()
beschneidet den Stream
auf die Anzahl Elemente, die
beim Methodenaufruf als Argument angegeben wird (hier 10) und
liefert wiederum einen IntStream
. Dessen Werte
schließlich werden durch die terminale Methode sum()
addiert.
Ein Hoch der Faulheit
Als Faulheit (Laziness) wird die (vielleicht unerwartete) Art des Verhaltens von Streams bei der Abarbeitung seiner Elemente bezeichnet.
Schaut man sich die Methode filter()
etwas genauer
an, so erkennt man, dass sie als Argument ein Predicate
erwartet. Dies ist ein functional interface dessen
funktionale Methode einen boolschen Wert liefert. Der folgende
Quelltext liefert somit einen Fehler, da kein Rückgabewert
erzeugt wird:
Stream.of(1, 68, 17, 104, 15).filter(i -> System.out.println("filter: " + i); // Fehler
Fügt man einen Rückgabewert hinzu, so wird der Quelltext zwar akzeptiert, erzeugt erstaulicherweise jedoch keine Ausgabe.
Stream.of(1, 68, 17, 104, 15).filter(i -> { System.out.println("filter: " + i); return true; });
Die Ursache besteht darin, dass intermediäre Methoden nur
ausgeführt werden, wenn eine terminale Operation vorhanden
ist. Damit nicht genug, auch die Reihenfolge der Abarbeitung ist
erstaunlich. Variieren und erweitern wir den Quelltext etwas und
fügen eine zweite filter()
-Methode hinzu.
Stream.of(1, 68, 9, 104, 15).filter(i -> { System.out.println("filter 1: " + i); return i > 10; }).filter(i -> { System.out.println("filter 2: " + i); return i % 3 == 0; }).forEach(i -> System.out.println("forEach: " + i));
Die Ausgabe verblüfft:
filter 1: 1 filter 1: 68 filter 2: 68 filter 1: 9 filter 1: 104 filter 2: 104 filter 1: 15 filter 2: 15 forEach: 15
Anders als man vielleicht erwarten würde, wird nicht
zunächst die erste Methode in der Kette für alle Werte
ausgeführt, dann die zweite, etc., sondern nacheinander
wird die gesamte Pipeline für jeden einzelnen Wert
durchlaufen. Wird eine Bedingung nicht erfüllt, wie es hier
beim ersten filter()
für die Werte 1 und 9 der
Fall ist, so werden die Folgemethoden gar nicht erst in Angriff
genommen.
Entsprechend wird die terminale Operation nur dann
ausgeführt, wenn die Kette der zuvor durchlaufenen filter()
-Methoden
jeweils true
ergeben hat. Dies ist hier nur
für den letzten Wert, 15, der Fall.
Java 9 Erweiterungenv.9.0
In Java 9 ist das Stream
-Interface etwas erweitert
worden. Die Methode takeWhile(boolean b)
verarbeitet Stream
-Elemente so lange wie b =
true
ist. Anhand des folgenden Beispiels ist zu
erkennen, dass die Verarbeitung abbricht, sobald ein Element die
Bedingung nicht erfüllt hat:
Stream.of(96, 168, 9, 104, 15).takeWhile(i->i%3==0).forEach(System.out::println);
104 wird nicht mehr akzeptiert, da die Zahl nicht ganzzahlig durch 3 zu dividieren ist, sodass die nachfolgende 15 ebenfalls nicht behandelt wird.
96 168 9
Die Methode dropWhile(boolean b)
überspringt
eine Verarbeitung, solange eine Bedingung erfüllt ist und
führt sie danach weiter aus:
Stream.of(96, 168, 9, 104, 15).dropWhile(i->i%3==0).forEach(System.out::println);
Das Beispiel liefert die Ausgabe:
104 15
Auch hier ist zu sehen, dass das Überspringen nur die ersten Werte betrifft, die durch 3 ganzzahlig teilbar sind. Wird die boolsche Bedingung in der Folge wiederum erfüllt, wie hier beim letzten Wert 15, so hat dies keinen weiteren Einfluss auf die Verarbeitung.
Die oben bereits betrachtete Methode iterate()
wurde in Java 9 erweitert. Eine überladene Variante kann
nun mit einer Abbruchbedingung versehen werden.
Stream.iterate(0, i->i<10, i->i+1).forEach(System.out::println);