Java 8 - Der beste Weg, eine Liste zu transformieren: map oder foreach?

188

Ich habe eine Liste, myListToParsein der ich die Elemente filtern, eine Methode auf jedes Element anwenden und das Ergebnis in eine andere Liste einfügen möchte myFinalList.

Mit Java 8 habe ich festgestellt, dass ich das auf zwei verschiedene Arten tun kann. Ich möchte den effizienteren Weg zwischen ihnen kennen und verstehen, warum ein Weg besser ist als der andere.

Ich bin offen für Vorschläge zu einem dritten Weg.

Methode 1:

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

Methode 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 
Emilien Brigand
quelle
55
Der zweite. Eine ordnungsgemäße Funktion sollte keine Nebenwirkungen haben. In Ihrer ersten Implementierung ändern Sie die Außenwelt.
ThanksForAllTheFish
37
nur eine Frage des Stils, elt -> elt != nullkann aber ersetzt werden durchObjects::nonNull
the8472
2
@ the8472 Noch besser wäre es, sicherzustellen, dass die Sammlung überhaupt keine Nullwerte enthält, und sie Optional<T>stattdessen in Kombination mit zu verwenden flatMap.
Herman
2
@ EnzymonRoziewski, nicht ganz. Für etwas so Triviales wie dieses wird die Arbeit, die erforderlich ist, um den Parallelstrom unter der Haube einzurichten, die Verwendung dieses Konstrukts stumm schalten.
MK
2
Beachten Sie, dass Sie unter der .map(this::doSomething)Annahme schreiben können, dass doSomethinges sich um eine nicht statische Methode handelt. Wenn es statisch ist, können Sie es durch thisden Klassennamen ersetzen .
Herman

Antworten:

153

Machen Sie sich keine Sorgen über Leistungsunterschiede, diese sind in diesem Fall normalerweise minimal.

Methode 2 ist vorzuziehen, weil

  1. Es ist nicht erforderlich, eine Sammlung zu mutieren, die außerhalb des Lambda-Ausdrucks existiert.

  2. Es ist besser lesbar, da die verschiedenen Schritte, die in der Erfassungspipeline ausgeführt werden, nacheinander geschrieben werden: zuerst eine Filteroperation, dann eine Kartenoperation und dann das Erfassen des Ergebnisses (weitere Informationen zu den Vorteilen von Erfassungspipelines finden Sie in Martin Fowlers ausgezeichnetem Artikel ).

  3. Sie können die Art und Weise, wie Werte erfasst werden, leicht ändern, indem Sie die ersetzen Collector verwendeten . In einigen Fällen müssen Sie möglicherweise Ihre eigenen schreiben Collector, aber der Vorteil ist, dass Sie diese problemlos wiederverwenden können.

ihr Mann
quelle
43

Ich stimme den vorhandenen Antworten zu, dass die zweite Form besser ist, weil sie keine Nebenwirkungen hat und einfacher zu parallelisieren ist (verwenden Sie einfach einen parallelen Stream).

In Bezug auf die Leistung scheinen sie gleichwertig zu sein, bis Sie parallele Streams verwenden. In diesem Fall ist die Leistung der Karte sehr viel besser. Siehe unten die Ergebnisse des Mikro-Benchmarks :

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

Sie können das erste Beispiel nicht auf die gleiche Weise verbessern , da forEach eine Terminalmethode ist - es gibt void zurück - und Sie gezwungen sind, ein statusbehaftetes Lambda zu verwenden. Aber das ist wirklich eine schlechte Idee, wenn Sie parallele Streams verwenden .

Beachten Sie schließlich, dass Ihr zweites Snippet mit Methodenreferenzen und statischen Importen etwas präziser geschrieben werden kann:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
Assylien
quelle
1
In Bezug auf die Leistung gewinnt in Ihrem Fall "map" wirklich "forEach", wenn Sie parallelStreams verwenden. Meine benchmaks in Millisekunden: SO28319064.forEach: 187.310 ± 1.768 ms / op - SO28319064.map: 189.180 ± 1.692 ms / op --SO28319064.mapParallelStream: 55.577 ± 0782 ms / op
Giuseppe Bertone
2
@ GiuseppeBertone, es liegt an Assylias, aber meiner Meinung nach widerspricht Ihre Bearbeitung der Absicht des ursprünglichen Autors. Wenn Sie Ihre eigene Antwort hinzufügen möchten, ist es besser, sie hinzuzufügen, als die vorhandene so oft zu bearbeiten. Auch jetzt ist der Link zum Mikrobenchmark für die Ergebnisse nicht relevant.
Tagir Valeev
5

Einer der Hauptvorteile der Verwendung von Streams besteht darin, dass Daten deklarativ verarbeitet werden können, dh mithilfe eines funktionalen Programmierstils. Es bietet auch kostenlose Multithreading-Funktionen, sodass kein zusätzlicher Multithreading-Code geschrieben werden muss, damit Ihr Stream gleichzeitig ausgeführt wird.

Angenommen, Sie untersuchen diesen Programmierstil, weil Sie diese Vorteile nutzen möchten, dann ist Ihr erstes Codebeispiel möglicherweise nicht funktionsfähig, da die foreachMethode als terminal eingestuft wird (was bedeutet, dass sie Nebenwirkungen hervorrufen kann).

Der zweite Weg wird aus Sicht der funktionalen Programmierung bevorzugt, da die Kartenfunktion zustandslose Lambda-Funktionen akzeptieren kann. Genauer gesagt sollte das an die Kartenfunktion übergebene Lambda sein

  1. Nicht störend, dh die Funktion sollte die Quelle des Streams nicht ändern, wenn sie nicht gleichzeitig auftritt (z ArrayList. B. ).
  2. Statuslos, um unerwartete Ergebnisse bei der Parallelverarbeitung zu vermeiden (verursacht durch Unterschiede in der Thread-Planung).

Ein weiterer Vorteil des zweiten Ansatzes besteht darin, dass, wenn der Strom parallel ist und der Kollektor gleichzeitig und ungeordnet ist, diese Eigenschaften nützliche Hinweise für die Reduktionsoperation liefern können, um das Sammeln gleichzeitig durchzuführen.

MK
quelle
4

Wenn Sie Eclipse-Sammlungen verwenden , können Sie die collectIf()Methode verwenden.

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

Es wird eifrig ausgewertet und sollte etwas schneller sein als die Verwendung eines Streams.

Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.

Craig P. Motlin
quelle
1

Ich bevorzuge den zweiten Weg.

Wenn Sie auf die erste Weise einen parallelen Stream verwenden, um die Leistung zu verbessern, haben Sie keine Kontrolle über die Reihenfolge, in der die Elemente der Ausgabeliste von hinzugefügt werden forEach.

Bei Verwendung toListbehält die Streams-API die Reihenfolge bei, auch wenn Sie einen parallelen Stream verwenden.

Eran
quelle
Ich bin mir nicht sicher, ob dies der richtige Rat ist: Er könnte ihn verwenden, forEachOrderedanstatt forEacheinen parallelen Stream zu verwenden, aber dennoch die Reihenfolge beizubehalten. Aber als Dokumentation für forEachStaaten opfert die Beibehaltung der Begegnungsreihenfolge den Vorteil der Parallelität. Ich vermute, dass dies auch dann der Fall toListist.
Hermann
0

Es gibt eine dritte Option - Verwenden stream().toArray()- siehe Kommentare unter Warum kein Stream eine toList-Methode hatte . Es ist langsamer als forEach () oder collect () und weniger ausdrucksstark. Es könnte in späteren JDK-Builds optimiert werden, also fügen Sie es hier für alle Fälle hinzu.

unter der Annahme List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

mit einem Mikro-Mikro-Benchmark, 1 Million Einträgen, 20% Nullen und einer einfachen Transformation in doSomething ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

Die Ergebnisse sind

parallel:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

sequentiell:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

parallel ohne Nullen und Filter (so ist der Stream SIZED): toArrays hat in diesem Fall die beste Leistung und .forEach()schlägt mit "indexOutOfBounds" auf der Empfänger-ArrayList fehl, die durch ersetzt werden musste.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}
Harshtuna
quelle
0

Kann Methode 3 sein.

Ich ziehe es immer vor, die Logik getrennt zu halten.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());
Kumar Abhishek
quelle
0

Wenn die Verwendung von 3rd Pary Libaries in Ordnung ist, definiert cyclops-react Lazy Extended Collections mit dieser integrierten Funktionalität. Zum Beispiel könnten wir einfach schreiben

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doSomething (elt));

myFinalList wird erst beim ersten Zugriff ausgewertet (und dort, nachdem die materialisierte Liste zwischengespeichert und wiederverwendet wurde).

[Offenlegung Ich bin der Hauptentwickler von Cyclops-React]

John McClean
quelle