Zwischenstromoperationen werden nicht nach Anzahl ausgewertet

33

Es scheint, dass ich Probleme habe zu verstehen, wie Java Stream-Operationen zu einer Stream-Pipeline zusammensetzt.

Bei der Ausführung des folgenden Codes

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Die Konsole druckt nur 4. Das StringBuilderObjekt hat noch den Wert "".

Wenn ich die Filteroperation hinzufüge: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Die Ausgabe ändert sich zu:

4
1234

Wie ändert diese scheinbar redundante Filteroperation das Verhalten der zusammengesetzten Stream-Pipeline?

Atalantus
quelle
2
Interessant !!!
uneq95
3
Ich würde mir vorstellen, dass dies ein implementierungsspezifisches Verhalten ist. Vielleicht liegt es daran, dass der erste Stream eine bekannte Größe hat, der zweite jedoch nicht, und die Größe bestimmt, ob die Zwischenoperationen ausgeführt werden.
Andy Turner
Was passiert aus Interesse, wenn Sie den Filter und die Karte umkehren?
Andy Turner
Nachdem ich ein bisschen in Haskell programmiert habe, riecht es ein bisschen nach einer faulen Auswertung, die hier stattfindet. Eine Google-Suche kehrte zurück, dass Streams tatsächlich etwas faul sind. Könnte das der Fall sein? Und ohne Java muss das Mapping nicht ausgeführt werden, wenn Java clever genug ist.
Frederik
@AndyTurner Es gibt das gleiche Ergebnis, auch bei Umkehrung
uneq95

Antworten:

39

Die count()Terminaloperation führt in meiner Version des JDK den folgenden Code aus:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Wenn sich eine filter()Operation in der Pipeline von Operationen befindet, kann die Größe des Streams, die anfangs bekannt ist, nicht mehr bekannt sein (da filtereinige Elemente des Streams abgelehnt werden könnten). Der ifBlock wird also nicht ausgeführt, die Zwischenoperationen werden ausgeführt und der StringBuilder wird somit geändert.

Wenn Sie jedoch nur map()in der Pipeline sind, entspricht die Anzahl der Elemente im Stream garantiert der ursprünglichen Anzahl der Elemente. Der if-Block wird also ausgeführt und die Größe wird direkt zurückgegeben, ohne die Zwischenoperationen auszuwerten.

Beachten Sie, dass das übergebene Lambda map()gegen den in der Dokumentation definierten Vertrag verstößt: Es soll sich um eine nicht störende, zustandslose Operation handeln, die jedoch nicht zustandslos ist. Ein unterschiedliches Ergebnis in beiden Fällen kann daher nicht als Fehler angesehen werden.

JB Nizet
quelle
flatMap()War dies der Grund, warum es anfangs eifrig (jetzt faul) war, weil es möglicherweise in der Lage war, die Anzahl der Elemente zu ändern? Die Alternative wäre also, forEach()separat zu verwenden und zu zählen, wenn map()in der aktuellen Form der Vertrag verletzt wird, denke ich.
Frederik
3
In Bezug auf flatMap denke ich nicht. Es war, AFAIK, weil es anfangs einfacher war, es eifrig zu machen. Ja, die Verwendung eines Streams mit map () zur Erzeugung von Nebenwirkungen ist eine schlechte Idee.
JB Nizet
Hätten Sie einen Vorschlag, wie Sie die volle Ausgabe erzielen können, 4 1234ohne den zusätzlichen Filter zu verwenden oder Nebenwirkungen in der map () -Operation zu erzeugen?
Atalantus
1
int count = array.length; String result = String.join("", array);
JB Nizet
1
oder Sie könnten forEach verwenden, wenn Sie wirklich einen StringBuilder verwenden möchten, oder Sie könntenCollectors.joining("")
njzk2
19

In jdk-9 wurde es in Java-Dokumenten klar dokumentiert

Das Eliminieren von Nebenwirkungen kann ebenfalls überraschend sein. Mit Ausnahme der Terminaloperationen forEach und forEachOrdered werden Nebenwirkungen von Verhaltensparametern möglicherweise nicht immer ausgeführt, wenn die Stream-Implementierung die Ausführung von Verhaltensparametern optimieren kann, ohne das Ergebnis der Berechnung zu beeinflussen. (Für ein konkretes Beispiel siehe Hinweis API auf dem dokumentierten Zählung Betrieb.)

API-Hinweis:

Eine Implementierung kann sich dafür entscheiden, die Stream-Pipeline (entweder sequentiell oder parallel) nicht auszuführen, wenn sie in der Lage ist, die Anzahl direkt von der Stream-Quelle zu berechnen. In solchen Fällen werden keine Quellelemente durchlaufen und keine Zwischenoperationen ausgewertet. Verhaltensparameter mit Nebenwirkungen, von denen bis auf harmlose Fälle wie das Debuggen dringend abgeraten wird, können betroffen sein. Betrachten Sie beispielsweise den folgenden Stream:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Die Anzahl der Elemente, die von der Stream-Quelle, einer Liste, abgedeckt werden, ist bekannt, und die Zwischenoperation peek injiziert oder entfernt keine Elemente aus dem Stream (wie dies bei flatMap- oder Filteroperationen der Fall sein kann). Somit ist die Anzahl die Größe der Liste und es besteht keine Notwendigkeit, die Pipeline auszuführen und als Nebeneffekt die Listenelemente auszudrucken.

Totes Schwimmbad
quelle
0

Dafür ist .map nicht gedacht. Es soll verwendet werden, um einen Stream von "Something" in einen Stream von "Something Else" zu verwandeln. In diesem Fall verwenden Sie map, um eine Zeichenfolge an einen externen Stringbuilder anzuhängen. Anschließend haben Sie einen Stream von "Stringbuilder", der jeweils durch die Map-Operation erstellt wurde, indem eine Nummer an den ursprünglichen Stringbuilder angehängt wird.

Ihr Stream macht eigentlich nichts mit zugeordneten Ergebnissen im Stream, daher ist es durchaus vernünftig anzunehmen, dass der Schritt vom Stream-Prozessor übersprungen werden kann. Sie rechnen bei der Arbeit mit Nebenwirkungen, die das Funktionsmodell der Karte beschädigen. Sie sollten besser mit forEach bedient werden, um dies zu tun. Führen Sie die Zählung vollständig als separaten Stream durch oder setzen Sie einen Zähler mit AtomicInt in forEach.

Der Filter zwingt ihn, den Stream-Inhalt auszuführen, da er nun mit jedem Stream-Element etwas fiktiv Bedeutendes tun muss.

DaveB
quelle