Mit Java 8 wurde die neue Stream API eingeführt. Dies ist keine neue Version der IO-Streams sondern eine API zur Verarbeitung von Listen von Daten. Das Ganze ist als fluent API umgesetzt und kann sehr schön zusammen mit Lambdas verwendet werden.
Was sind Streams?
Streams sind, wie oben bereits erwähnt, nicht mit den IO-Streams (java.io.InputStream, java.io.OutputStream) zu verwechseln. Die hier genannten Streams stellen eine Strom von Daten eines bestimmten Typs dar, auf dem sich verschiedene Operationen ausführen lassen.
Ein Vorteil ist, daß Code in der Regel kürzer, übersichtlicher und leicht verständlicher wird. Zudem übernimmt die API auch die parallele Verarbeitung der Daten, falls die Art des Streams und der Use Case dies erlauben.
Erste Schritte
Streams können bspw. aus bestehenden Collections (List, Set, …) erzeugt werden oder über die of()-Methoden des Stream-Interfaces:
final List<String> list = Arrays.asList("Hello", "World", "Java", "stream"); final Stream<String> streamFromCollection = list.stream(); streamFromCollection.forEach(System.out::println); final Stream<String> streamViaOf = Stream.of("foo", "bar", "x", "y"); streamViaOf.forEach(System.out::println);
Hier sehen wir gleich die erste Operation auf Streams: forEach. Diese Methode ruft einen Consumer (hier die println()-Methode) für jedes Element im Stream auf. Wichtig ist, daß forEach keine Möglichkeit hat, den Stream zu verändern.
collect(), Collector
In vielen Fällen ist es nötig, den Stream in etwas anderes, z. B. eine Collection umzuwandeln:
final Stream<String> stream = Stream.of("foo", "bar", "x", "y"); final List<String> list = new ArrayList<String>(); stream.forEach(x -> list.add(x));
Funktioniert, geht aber wesentlich eleganter, wenn ein Collector eingesetzt wird:
final Stream<String> stream = Stream.of("foo", "bar", "x", "y"); final List<String> list = stream.collect(Collectors.toList());
Ein Collector führt eine Transformationsoperation auf dem Stream aus und kann unterschiedlichste Ergebnisse wie den minimalen/maximalen Wert, Summe aller Elemente, Durchschnitt aller Elemente, oder eben die Elemente des Streams als Liste. Die Klasse Collectors stellt dabei eine Reihe vordefinierten Kollektoren bereit.
map(), filter()
Bisher haben wir gesehen, wie Streams erzeugt und umgewandelt werden können und wie eine Aktion auf jedes Element im Stream angewendet werden kann. Interessant werden Streams durch die Operationen, die auf den Stream-Elementen ausgeführt werden können, die neue Streams mit neuen Elemente erzeugen.
Die Methode map() führt eine Transformation auf jedem Stream-Element aus. So können bspw. mit
final Stream<String> stream = Stream.of("foo", "bar", "x", "y"); stream .map(x -> x.toUpperCase()) .forEach(System.out::println);
alle String im Stream in Großbuchstaben verwandelt werden.
Die Methode filter() übernimmt nur die Elemente in den neuen Stream, die eine bestimmte Bedingung erfüllen:
final Stream<String> stream = Stream.of("foo", "bar", "x", "y"); stream .filter(x -> x.length() < 3) .forEach(System.out::println);
Dieses Beispiel filtert alle Elemente aus, deren Länge 3 oder größer, so daß der resultierende Stream nur Elemente enthält, die kürzer als 3 sind.
Die Methoden lassen sich auch kombinieren. Das nachfolgende Beispiel liefert alle Elemente die kürzer als 3 Zeichen sind in Großbuchstaben:
final Stream<String> stream = Stream.of("foo", "bar", "x", "y"); stream .filter(x -> x.length() < 3) .map(x -> x.toUpperCase()) .forEach(System.out::println);
skip(), limit()
Mit den beiden Methoden skip() und limit() lässt sich die Abarbeitung beeinflussen.
skip(n) überspringt die nächsten n Elementen im Stream:
final Stream<Integer> stream = IntStream.range(0, 110).boxed(); stream .skip(100) .forEach(System.out::println);
IntStream.range(0, 110) erzeugt einen Stream von 110 int-Werten beginnend mit 0. Das dieser den Typ IntStream hat, erfolgt eine Überführung in den Typ Stream<Integer> durch Aufruf von boxed(). skip(100) überspring die ersten 100 Elemente des Stream und gibt den Rest (also 100, 101, …,109) aus.
limit(n) erzeugt einen neuen Stream, der die nächsten n Elemente des ursprünglichen Streams enthält. Besaß der ursprüngliche Stream weniger Elemente, so enthält der resultierende Stream alle diese Elemente:
final Stream<Integer> stream = IntStream.range(0, 110).boxed(); stream .limit(5) .forEach(System.out::println);
Auch diese Aufrufe lassen sich kombinieren. Hier ein etwas komplizierteres Beispiel:
final Stream<Integer> stream = IntStream.range(0, 110).boxed(); stream .skip(5) .limit(33) .filter(x -> x % 2 == 0) .map(x -> x * 3) .forEach(System.out::println);
Was passiert hier: Aus dem Stream mit den Werten 0 … 109 werden die ersten 5 Elemente übersprungen, der verbleibende Stream auf 33 Elemente begrenzt. Bleiben also die Werte 5 … 37. Anschliessend werden alle ungeraden Zahlen ausgefiltert, so daß nur die geraden Zahlen übrig bleiben (6, 8, 10, … 36). Am Ende werden die verbleibenden Zahlen mit 3 multipliziert. Die Ausgabe umfasst damit die Zahlen 18, 24, …, 108.
So könnte übrigens der Code dazu ohne Streams aussehen:
// IntStream.range(...).boxed() final List<Integer> list = new ArrayList<>(); for(int i = 0; i < 110; i++) list.add(i); // skip(5) for(int i = 0; i < 5; i++) list.remove(0); // limit(33) while(list.size() > 33) list.remove(33); // filter(...) and map(...) final List<Integer> list2 = new ArrayList<>(); for(final int i : list) if(i % 2 == 0) list2.add(i * 3); // forEach(...) for(final int i : list2) System.out.println(i);
sequential(), parallel()
Bei Streams, die mittels Collection.stream() erzeugt wurden, werden die Elemente sequentiell abgearbeitet. Streams unterstützten in der Regel auch die parallele Verarbeitung ihrer Elemente. Solche Streams werden durch die Methode parallel() erzeugt. Die Verarbeitung erfolgt jetzt parallel in verschiedenen Threads.
final Stream<Integer> stream = IntStream.range(0, 10).boxed(); stream .parallel() .forEach(System.out::println);
Hier wird ein Stream mit den Werten 0 … 9 erzeugt. Ohne parallel() würde die Werte in aufsteigender Reihenfolge ausgegeben. Durch die parallele Verarbeitung ist die Reihenfolge nicht mehr vorhersagbar.
Der Aufruf von sequential() erzeugt Stream, dessen Elemente nacheinander verarbeitet werden.
sorted()
Diese Methode liefert einen Stream, dessen Elemente sortiert sind. Das folgende Beipspiel zeigt, wie int-Werte in absteigender Reihefolge sortiert werden können:
final Stream<Integer> stream = IntStream.range(0, 10).boxed(); stream .sorted((x,y) -> -Integer.compare(x, y)) .forEach(System.out::println);
Vorsicht: sorted() funktioniert nicht bei Streams, die mittels parallel() erzeugt wurden!
In solchen Fällen müssen mit sequential() wieder sequentielle Streams erzeugt werden und auf diesen sorted() aufgerufen werden.
Fazit
Mit der Stream API wurde ein mächtiges Werkzeug ins JDK aufgenommen. Streams machen den Code lesbarer und leicht verständlicher. Durch den passenden Einsatz von Lambda-Ausdrücken können Implementierungen stark vereinfacht werden.