Suche
Faul oder nicht faul ... ?

Sind Java 8 Streams wirklich Lazy? Nicht ganz!

Lukas Eder

© Shutterstock / Kanyanat Wimonkanjana

 

Wie gut kennen Sie die Streams aus Java 8? Ein interessantes Detail dazu hat Lukas Eder in dieser Kolumne herausgefunden. Denn so ganz nach dem „Lazy“-Prinzip funktionieren die Streams dann doch nicht…

Sind Java 8 Streams wirklich Lazy?

In einem Artikel habe ich kürzlich gezeigt, dass Entwickler beim Arbeiten mit Streams immer der Strategie folgen sollten, zuerst den Filter anzuwenden, danach erst das Mapping. Das folgende Beispiel zeigt die Idee:

hugeCollection
    .stream()
    .limit(2)
    .map(e -> superExpensiveMapping(e))
    .collect(Collectors.toList());

In diesem Beispiel implementiert die Operation limit() den Filter, der vor dem Mapping angewandt werden sollte.

Einige Leser haben richtigerweise bemerkt, dass in diesem Fall die Reihenfolge keine Rolle spielt, in der wir die limit() und map() Operationen ausführen. Der Grund dafür ist, dass die meisten Operationen im Java 8 Stream API „lazy“ evaluiert werden.

Oder genauer: Die abschließende collect()-Operation zieht sich die Werte „lazy“ vom Stream. Und wenn die Anweisung limit sein Ende erreicht, werden keine neuen Werte mehr produziert, egal ob nun map() zuvor oder danach ausgeführt wird. Das kann leicht durch das folgende Beispiel demonstriert werden:

import java.util.stream.Stream;
 
public class LazyStream {
    public static void main(String[] args) {
        Stream.iterate(0, i -> i + 1)
              .map(i -> i + 1)
              .peek(i -> System.out.println("Map: " + i))
              .limit(5)
              .forEach(i -> {});
 
        System.out.println();
        System.out.println();
 
        Stream.iterate(0, i -> i + 1)
              .limit(5)
              .map(i -> i + 1)
              .peek(i -> System.out.println("Map: " + i))
              .forEach(i -> {});
    }
}

Der Output sieht hier so aus:

Map: 1
Map: 2
Map: 3
Map: 4
Map: 5

Map: 1
Map: 2
Map: 3
Map: 4
Map: 5

Aber das ist nicht immer so!

Diese Optimierung ist allerdings ein Implementierungsdetail. Allgemein ist es nicht unklug, wirklich immer den Grundsatz zu beachten, zuerst den Filter, dann das Mapping anzuwenden, und sich nicht auf eine Optimierung wie oben zu verlassen. Insbesondere funktioniert die Java-8-Implementierung von flatMap() nicht nach dem „Lazy“-Prinzip.

Schauen wir uns einmal das folgende Snippet an, in dem wir eine flatMap()-Operation in die Mitte des Streams setzen:

import java.util.stream.Stream;
 
public class LazyStream {
    public static void main(String[] args) {
        Stream.iterate(0, i -> i + 1)
              .flatMap(i -> Stream.of(i, i, i, i))
              .map(i -> i + 1)
              .peek(i -> System.out.println("Map: " + i))
              .limit(5)
              .forEach(i -> {});
 
        System.out.println();
        System.out.println();
 
        Stream.iterate(0, i -> i + 1)
              .flatMap(i -> Stream.of(i, i, i, i))
              .limit(5)
              .map(i -> i + 1)
              .peek(i -> System.out.println("Map: " + i))
              .forEach(i -> {});
    }
}

Das Ergebnis sieht hier folgendermaßen aus:

Map: 1
Map: 1
Map: 1
Map: 1
Map: 2
Map: 2
Map: 2
Map: 2

Map: 1
Map: 1
Map: 1
Map: 1
Map: 2

Wir sehen, dass die erste Stream Pipeline noch vor der Anwendung von limit(5) das Mapping für alle 8 Werte des Flatmappings vornimmt. Hingegen begrenzt die zweite Stream Pipeline den Stream tatsächlich zuerst auf die 5 Elemente, um danach nur für diese das Mapping zu starten.

Der Grund dafür liegt in der Implementierung von flatMap():

// In ReferencePipeline.flatMap()
try (Stream<? extends R> result = mapper.apply(u)) {
    if (result != null)
        result.sequential().forEach(downstream);
}

Man sieht, dass das Ergebnis der flatMap()-Operation immer sofort von einem abschließenden forEach() weiterverwendet wird. In unserem Fall werden so immer alle vier Werte produziert und zur nächsten Anweisung geschickt. Folglich ist flatMap() nicht „lazy“, und die nächste Operation erhält alle seine Ergebnisse.

Das trifft so für Java 8 zu. Doch könnten zukünftige Java-Versionen dies ändern.

Ergo:

Besser wir filtern die Daten zuerst, danach führen wir das Mapping durch.

 

Verwandte Themen:

Geschrieben von
Lukas Eder
Lukas Eder
Lukas Eder ist leidenschaftlicher Java- und SQL-Entwickler. Er ist Gründer und Leiter der Forschungs- und Entwicklungsabteilung der Data Geekery GmbH, dem Unternehmen hinter jOOQ - die einfachste Art, um SQL in Java zu schreiben.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.