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 mehrdimensionalen Arrays
Analog zum oben gesagten, kann der Inhalt eines zweidimensionalen Arrays auf zweierlei Arten ausgegeben werden:
int[][] ints = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; Arrays.stream(ints).forEach((i) -> { Arrays.stream(i).forEach((j) -> System.out.print(j + " ")); });
und
Stream.of(ints).forEach((i) -> { Arrays.stream(i).forEach((j) -> System.out.print(j + " ")); });
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.
Dies lenkt den Blick auf eine generelle Eigenschaft von Streams, die immer bedacht werden muss: Tritt innerhalb der Abarbeitung eines Streams ein Fehler auf, so werden die nachfolgenden Funktionen nicht mehr ausgeführt.
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);
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.