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 und 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 IntStream zudem Methoden zur Weiterverarbeitung primitiver int-Werte (sum(), average(), count() , etc.).

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 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

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.