Streams erlauben die Ausführung von Operationen auf Arrays und Listen.v.8.0

In Java 8 wurden mit dem Interface java.util.stream.Stream<T> mächtige Möglichkeiten zur Durchführung von Operationen auf Arrays und Listen eingeführt.

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:

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.