Java 8: IntStream – der Stream-Macher

von Hubert Schmid vom 2014-05-18

Java 8 hat vier unterschiedliche Schnittstellen für Streams eingeführt: Die generische Schnittstelle Stream<T> für Referenztypen, sowie die drei Schnittstellen IntStream, LongStream und DoubleStream für primitive Typen. Doch eine dieser Schnittstellen ist besonders: Was die Zählschleife unter den Schleifen ist, ist der IntStream unter den Streams. Es folgen fünf Beispiele für die Verwendung.

Bezeichnend

Üblicherweise verwendet man einen Stream über einen Typen T, um Auswertungen über Elemente dieses Typs zu machen. Eine Entsprechung für ints ist im folgenden Listing zu sehen. Dabei wird der Stream verwendet, um die Summe der Ganzzahlen aus dem angegebenen, halboffenen Interval zu bestimmen, die eine gewisse Eigenschaft erfüllen.

int evenSum(int fromInclusive, int toExclusive) { return IntStream.range(fromInclusive, toExclusive) .filter(i -> i % 3 != 0) .sum(); }

Die Implementierung entspricht also im Wesentlichen einer normalen for-Schleife mit einer verschachtelten if-Bedingung.

Indizierend

Andererseits lassen sich zählende for-Schleifen auch verwenden, um mittels Index über die Elemente eines Arrays zu iterieren. Analog dazu kann man ein IntStream verwenden, um aus einem Array einen Stream zu erzeugen. Eine entsprechende Implementierung findet sich im folgenden Listing.

<T> Stream<T> makeStream(T[] array) { return IntStream.range(0, array.length) .mapToObj(i -> array[i]); }

Für den produktiven Einsatz ist dieser Einzeiler bereits qualitativ hinreichend. Man könnte sie sogar empfehlen, wenn es mit java.util.Arrays::stream nicht bereits eine geeignetere Entsprechung in der Standardbibliothek gäbe.

Skalierend

In den beiden vorherigen Beispielen wurde jeweils die Methode IntStream::range verwendet, um einen Stream sequentiell aufsteigender ints zu erzeugen. Für DoubleStream gibt es dazu keine Entsprechung. Doch mit Hilfe von IntStream lässt sich einfach eine dazu konstruieren, wie im folgenden Listing zu sehen.

DoubleStream range(int from, int to, double scale) { return IntStream.range(from, to) .mapToDouble(i -> i * scale); }

Der Aufruf range(0, 100, 0.01) liefert somit einen DoubleStream, der in Prozent-Schritten von 0.0 bis 1.0 (exklusive) läuft.

Kombinierend

Häufig werden Streams nur mit Elementen aus einer einzelnen Quelle verwendet. Doch was tun, wenn mehrere Quellen miteinander kombiniert werden müssen? Die Standardbibliothek von Java 8 bietet dafür direkt keine Lösung an. Doch auch hier kann der IntStream helfen. Das folgende Listing zeigt, wie man IntStream verwenden kann, um das Skalarprodukt zweier Vektoren im kartesischen Koordinatensystem zu berechnen.

double innerProduct(double[] lhs, double[] rhs) { int length = Math.max(lhs.length, rhs.length); return IntStream.range(0, length) .mapToDouble(i -> lhs[i] * rhs[i]) .sum(); }

Das Ganze funktioniert auch für andere Element- und Collection-Typen. Für die Container ist nur wichtig, dass sie einen effizienten Zugriff mittels Index unterstützten – wie beispielsweise bei der ArrayList.

Konvertierend

Spezifische Schnittstellen für Streams existieren nur für die primitiven Typen int, long und double. Für byte, short, char und float muss man dagegen auf irgendeine Art von Konvertierung zurückgreifen. Im folgenden Listing ist eine Implementierung für float-Arrays zu sehen, die jedoch nur auf den ersten Blick gut aussieht.

Stream<Float> makeStream(float[] array) { return IntStream.range(0, array.length) .mapToObj(i -> array[i]); }

Das Problem hierbei ist, dass für jedes Element potentiell ein neues Float-Objekt erzeugt wird. Das verursacht sowohl Overhead bei der Erzeugung als auch bei der Garbage-Collection. Besser wäre das float-Array zunächst in ein double-Array zu kopieren und daraus einen DoubleStream zu erzeugen. Noch besser ist allerdings die Konvertierung on-the-fly mit Hilfe eines IntStream durchzuführen, wie im folgenden Listing zu sehen.

DoubleStream makeStream(float[] array) { return IntStream.range(0, array.length) .mapToDouble(i -> array[i]); }

Der Trick besteht also wiederum darin, den IntStream zur Indizierung zu verwenden. In gewisser Weise handelt es sich hierbei um ein wiederkehrendes Muster – sowie die Zählschleife ein Muster der allgemeinen Schleife.