Tech up! Streams

Nachdem wir uns das letzte Mal mit Optionals beschäftigt haben, möchten wir uns heute in unserer “Java 9 Perlen“-Serie die Erweiterungen in der Stream-Klasse anschauen.

Als Vorbereitung starten wir mit einem Feature, das es bereits seit Java 8 gibt:

iterate() – Infinite sequential ordered Stream

API

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

Returns an infinite sequential ordered Stream produced by iterative application of a function f to an initial element seed, producing a Stream consisting of seed, f(seed), f(f(seed))}, etc.

Oder etwas einfacher gesagt: wir haben einen unendlichen Stream, bei dem die einzelnen Werte (ausgehend von einem Startwert) dynamisch berechnet werden. Als Beispiel implementieren wir einen Stream valuesStream, der die Werte 1 bis n berechnet, ausgehend vom Startwert 1 und der Funktion n+1.

Stream<Integer> valuesStream = Stream.iterate(
     1,               // Startwert
     val -> val + 1); // Berechnung nächster Wert

Wir können iterate() auch verwenden, um einen komplexeren unendlichen Stream zu berechnen. Als Beispiel berechnen wir die Fibonacci Zahlenreihe. Die mathematische Formel für die Fibonacci Reihe lautet:

f(0) = 0
f(1) = 1
f(n) = f(n -1) + f(n – 2)

Dies lässt sich dann mittels Streams folgendermassen umsetzen:

 
Stream<Pair<Long>> fibonacciStream = Stream.iterate(
     new Pair<Long>(0l, 1l),
     p -> new Pair<Long>(p.getSecond(), p.getFirst() + p.getSecond())
);

Wir starten mit einem Pair von f(0) und f(1) und berechnen in jedem Schritt ein Pair von f(n-2) und f(n-1). Die Fibonacci Reihe ist dann jeweils das erste Element jedes Pairs.

Die Klasse Pair ist eine simple Helper Klasse die jeweils 2 Werte kapselt:

 
private static class Pair {
  private T first;
  private T second;
  public Pair(T first, T second) {
    this.first = first;
    this.second = second;
  }
  public T getFirst() {
    return first;
  }
  public T getSecond() {
    return second;
  }
}

Nach diesen Vorbereitungen nun zu den Stream Erweiterungen in Java 9.

takeWhile() – Content basierendes “limit” auf einem Stream

API

public Stream<T> takeWhile(Predicate<? super T> predicate)

Returns, if this stream is ordered, a stream consisting of the longest prefix of elements taken from this stream that match the given predicate. Otherwise returns, if this stream is unordered, a stream consisting of a subset of elements taken from this stream that match the given predicate.

Starten wir nochmals mit unserem valuesStream:

Stream valuesStream = Stream.iterate(1, val -> val + 1);

Wenn wir nur die ersten 10 Elemente haben möchten, können wir den Stream (seit Java 8) mittels limit(n) auf die Grösse n (Anzahl Elemente) beschränken. Mit n=10 werden dann die 10 ersten Werte unseres infinite Streams berechnet.

Stream first10Values = valuesStream.limit(10);

In Java 9 gibt es eine neue Methode takeWhile, die über den Stream iteriert, jeweils ein Predicate (“Filter”) anwendet und abbricht, sobald das Predicate false zurück liefert. So ist es möglich, einen Stream nicht positionsbasierend sondern content basierend einzuschränken.

Mit takeWhile können wir z.B. den valuesStream so einschränken, dass nur Werte die kleiner als 5 enthalten sind.

Stream valuesSmallerThan5 =
       values.stream().takeWhile(val -> val < 5) ;

Dies können wir nun gleich anwenden auf die Fibonacci Reihe, wenn wir all Zahlen bis zu einem bestimmten Wert (z.B. 100) berechnen möchten. Wir nehmen unseren fibonacciStream, limitieren die Grösse des Streams mittel eines Lambdas, holen uns die Fibonacci Werte (immer das erste Element jedes Pairs) und konvertieren die Zahlen (zur Veranschaulichung) in einen String:

String valuesSmallerThan100 = fibonacciStream
    .takeWhile(p -> p.getFirst() < 100)
    .map(p -> p.getFirst().toString())
    .collect(Collectors.joining(" | "));

Als Ausgabe erhalten wir:

0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89

dropWhile() – Content basierendes “limit” auf einem Stream

API

public Stream<T> dropWhile(Predicate<? super T> predicate)

Returns, if this stream is ordered, a stream consisting of the remaining elements of this stream after dropping the longest prefix of elements that match the given predicate. Otherwise returns, if this stream is unordered, a stream consisting of the remaining elements of this stream after dropping a subset of elements that match the given predicate.

Wir starten mit einem Stream mit 10 Elementen:

List<Integer> values = List.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

Mittels skip(n) können wir (schon seit Java 8) die ersten n Elemente ignorieren. Der neue Stream hat dann die Werte 5 bis 9.

List<Integer> skip5 = values.stream().skip(5);

Mit dropWhile (neu in Java 9) werden alle Werte ignoriert, solang das Predicate (“Filter”) true zurück liefert.

String valuesSmallerThan5 = values.stream().dropWhile(val -> val < 5);

Wir erhalten das gleiche Resultat wie bei skip5.

5 | 6 | 7 | 8 | 9

iterate() Version 2 – Iterate mit Abbruch Kriterium

API

public static<T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)

Returns a sequential ordered Stream produced by iterative application of the given next function to an initial element, conditioned on satisfying the given hasNext predicate. The stream terminates as soon as the hasNext predicate returns false.

Das neue iterate ist ein iterate mit einem Abbruch Kriterium, das mittels eines Predicates implementiert wird. Somit können wir “potentielle” unendliche Streams definieren, die dann aber auf eine gewisse Grösse beschränkt werden.

Ein Stream mit den Werten 0 bis 9 lässt sich dann folgendermassen implementieren:

String values0To9 = Stream.iterate(
     0, // Start Wert
     n -> n < 10, // Abbruch Kriterium
     n -> n + 1) // Berechnung nächster Wert

Die Fibonacci Zahlen bis 100 lässt sich mit der neuen iterate Methode auch so implementieren, dass wir die Werte direkt im Stream begrenzen, statt in einem späteren Schritt zu filtern bzw. einzuschränken:

Stream<Pair<Long>> fibonacciStreamUntil100 = Stream.iterate(
      new Pair<Long>(0l, 1l),
      p -> p.getFirst() < 100,
      p -> new Pair<Long>(p.getSecond(), p.getFirst() + p.getSecond()) );

Und zur Veranschaulichung konvertieren wir den Stream noch in einen String

String valuesSmallerThan100 = fibonacciStreamUntil100
        .map(p -> p.getFirst().toString())
        .collect(Collectors.joining(" | “));

und geben ihn aus:

0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89

Wie erwartet erhalten wir die gleichen Werte wie bei der Implementation mit iterate und takeWhile.

Das nächste Mal wollen wir uns zwei weitere Methoden im Stream API anschauen, die sich mit Null-Werten beschäftigen.

Referenz:

  • JavaDoc
  • https://stackoverflow.com/questions/30595844/java-8-lambda-expressions-for-solving-fibonacci-non-recursive-way
Kommentare sind geschlossen.