Zum Hauptinhalt springen

Die Java Stream API

Die Java Stream API ermöglicht die funktionale Verarbeitung von Elementfolgen. Ein Strom (Stream) repräsentiert eine Sequenz von Elementen, auf der verkettete Operationen ausgeführt werden können — entweder sequentiell oder parallel. Die Originaldaten bleiben dabei unverändert. Die Auswertung erfolgt nach dem Prinzip der Bedarfsauswertung (Lazy Evaluation): Operationen werden erst dann ausgeführt, wenn eine terminale Operation dies erfordert.

info

Ströme (Paket java.util.stream) haben nichts mit Datenströmen (IO-Streams) (Paket java.io) zu tun.

Erzeugen von Strömen

Ströme lassen sich aus Feldern, Listen, Mengen oder einzelnen Werten erzeugen.

MainClass.java
public class MainClass {

public static void main(String[] args) {
int[] array = {4, 8, 15, 16, 23, 42};
IntStream integerStream = Arrays.stream(array);

List<Integer> list = List.of(4, 8, 15, 16, 23, 42);
Stream<Integer> integerStream2 = list.stream();

Stream<Integer> integerStream3 = Stream.of(4, 8, 15, 16, 23, 42);
}

}

Im Gegensatz zu Stream<T> bieten die spezialisierten Klassen IntStream, DoubleStream und LongStream zusätzliche Methoden zur Verarbeitung primitiver Werte, wie etwa sum() oder average().

MainClass.java
public class MainClass {

public static void main(String[] args) {
int[] array = {4, 8, 15, 16, 23, 42};
IntStream integerStream = Arrays.stream(array);
int sum = integerStream.sum();
}

}

Intermediäre Operationen

Intermediäre Operationen transformieren einen Strom in einen neuen Strom. Sie werden erst dann ausgeführt, wenn eine terminale Operation folgt. Typische intermediäre Operationen sind Filtern, Abbilden und Sortieren.

OperationMethodeSchnittstellen-Methode
FilternStream<T> filter(predicate: Predicate<T>)boolean test(t: T)
AbbildenStream<R> map(mapper: Function<T, R>)R apply(t: T)
AbbildenDoubleStream mapToDouble(mapper: ToDoubleFunction<T, R>)double applyAsDouble(value: T)
AbbildenIntStream mapToInt(mapper: ToIntFunction<T, R>)int applyAsInt(value: T)
AbbildenLongStream mapToLong(mapper: ToLongFunction<T, R>)long applyAsLong(value: T)
SpähenStream<T> peek(consumer: Consumer<T>)void accept(t: T)
SortierenStream<T> sorted(comparator: Comparator<T>)int compare(o1: T, o2: T)
UnterscheidenStream<T> distinct()-
BegrenzenStream<T> limit(maxSize: long)-
ÜberspringenStream<T> skip(n: long)-

Terminale Operationen

Terminale Operationen schließen den Strom ab und liefern ein Ergebnis. Da der Strom danach nicht mehr verwendbar ist, können keine weiteren Operationen folgen. Typische Anwendungsfälle sind das Prüfen, Aggregieren und Sammeln von Elementen.

OperationMethodeSchnittstellen-Methode
FindenOptional<T> findAny()-
FindenOptional<T> findFirst()-
Prüfenboolean allMatch(predicate: Predicate<T>)boolean test(t: T)
Prüfenboolean anyMatch(predicate: Predicate<T>)boolean test(t: T)
Prüfenboolean noneMatch(predicate: Predicate<T>)boolean test(t: T)
AggregierenOptional<T> min(comparator: Comparator<T>)int compare(o1: T, o2: T)
AggregierenOptional<T> max(comparator: Comparator<T>)int compare(o1: T, o2: T)
Aggregierenlong count()-
SammelnR collect(collector: Collector<T, A, R>)-
Ausführenvoid forEach(action: Consumer<T>)void accept(t: T)

Zahlenströme (IntStream, DoubleStream, LongStream) bieten zusätzlich die terminalen Operationen sum() und average().

Bedarfsauswertung (Lazy Evaluation)

Bei der Bedarfsauswertung werden intermediäre Operationen nicht sofort ausgeführt, sondern erst dann, wenn eine terminale Operation den Strom abschließt. Zudem werden bei verketteten Operationen alle Schritte für jedes Element nacheinander durchlaufen — nicht erst alle Elemente durch Schritt 1, dann alle durch Schritt 2.

Das folgende Beispiel filtert den Zahlenstrom 4-8-15-16-23-42 zunächst nach geraden Zahlen, dann nach Zahlen größer als 15, und gibt die verbliebenen Zahlen aus. Zur Veranschaulichung wird jeder Filterschritt ebenfalls ausgegeben.

MainClass.java
public class MainClass {

public static void main(String[] args) {
Stream.of(4, 8, 15, 16, 23, 42).filter(i -> {
System.out.println(i + ": filter 1");
return i % 2 == 0;
}).filter(i -> {
System.out.println(i + ": filter 2");
return i > 15;
}).forEach(i -> System.out.println(i + ": forEach"));
}

}

Ohne Bedarfsauswertung würden die Operationen nacheinander für alle Elemente ausgeführt:

4: filter 1
8: filter 1
15: filter 1
16: filter 1
23: filter 1
42: filter 1
4: filter 2
8: filter 2
16: filter 2
42: filter 2
16: forEach
42: forEach

Aufgrund der Bedarfsauswertung werden alle Operationen für jedes Element einzeln nacheinander ausgeführt:

4: filter 1
4: filter 2
8: filter 1
8: filter 2
15: filter 1
16: filter 1
16: filter 2
16: forEach
23: filter 1
42: filter 1
42: filter 2
42: forEach

Unendliche Ströme

Die Java Stream API stellt Methoden bereit, mit denen sich theoretisch unendlich viele Elemente erzeugen lassen. In der Praxis werden solche Ströme durch limit() begrenzt.

  • Stream<T> iterate(seed: T, f: UnaryOperator<T>) — erzeugt einen unendlichen Strom aus einem Startwert und einer Funktion, die jeweils das nächste Element berechnet
  • Stream<T> iterate(seed: T, hasNext: Predicate<T>, next: UnaryOperator<T>) — wie oben, aber mit einer Abbruchbedingung
  • Stream<T> generate(s: Supplier<T>) — erzeugt Elemente über einen Lieferanten, z.B. Zufallszahlen
MainClass.java
public class MainClass {

public static void main(String[] args) {
// Zahlen 0 bis 99 ausgeben (unendlicher Strom, begrenzt auf 100)
Stream.iterate(0, i -> ++i).limit(100).forEach(System.out::println);
// Zahlen 0 bis 99 mit Abbruchbedingung
Stream.iterate(0, i -> i < 100, i -> ++i).forEach(System.out::println);
// 100 Pseudozufallszahlen von 0 bis 99
Stream.generate(() -> new Random().nextInt(100)).limit(100).forEach(System.out::println);
}

}

Die ersten beiden Ströme geben die Zahlen von 0 bis 99 aus. Der dritte erzeugt 100 Pseudozufallszahlen von 0 bis 99 und wird durch limit(100) begrenzt.