wake-up-neo.net

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

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

Mit Java 8 habe ich festgestellt, dass ich es auf zwei verschiedene Arten tun kann. Ich würde gerne wissen, wie man effizienter arbeitet und warum ein Weg besser ist als der andere. 

Ich bin offen für jeden Vorschlag über einen 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()); 
146
Emilien Brigand

Machen Sie sich keine Sorgen über Leistungsunterschiede, in diesem Fall sind sie 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, sequentiell geschrieben werden (zuerst eine Filteroperation, dann eine Kartenoperation und dann das Ergebnis).. Fowlers exzellenter Artikel

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

124
herman

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

In Bezug auf die Leistung scheint es, dass sie gleichwertig sind, bis Sie parallele Streams verwenden. In diesem Fall ist map wirklich viel besser. Sehen Sie unten die Mikro-Benchmark Ergebnisse:

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

Das erste Beispiel kann nicht auf die gleiche Weise angehoben werden, da forEach eine Terminalmethode ist - es gibt void zurück -, und Sie müssen ein Stateful-Lambda verwenden. Aber das ist wirklich eine schlechte Idee, wenn Sie parallele Streams verwenden .

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

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
38
assylias

Ein Hauptvorteil der Verwendung von Streams besteht darin, dass Daten deklarativ verarbeitet werden können, d. H. Mit einem funktionalen Programmierstil. Es gibt auch Multi-Threading-Funktionen für freie Bedeutung. Es ist nicht notwendig, zusätzlichen Multi-Threading-Code zu schreiben, um den Stream gleichzeitig zu machen.

Angenommen, Sie erforschen diesen Programmierstil, weil Sie diese Vorteile nutzen möchten. Ihr erstes Codebeispiel ist möglicherweise nicht funktionsfähig, da die foreach-Methode als terminal eingestuft wird (was bedeutet, dass sie Nebeneffekte erzeugen kann).

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

  1. Nicht störend, was bedeutet, dass die Funktion die Quelle des Streams nicht ändern sollte, wenn sie nicht gleichzeitig ist (z. B. ArrayList).
  2. Statuslos, um unerwartete Ergebnisse bei der parallelen Verarbeitung zu vermeiden (verursacht durch Unterschiede bei der Thread-Planung).

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

5
Mika'il

Wenn Sie Eclipse Collections 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 ein Stream.

Hinweis: Ich bin ein Committer für Eclipse Collections.

4
Craig P. Motlin

Ich bevorzuge den zweiten Weg.

Wenn Sie die erste Möglichkeit verwenden, einen parallelen Stream zu verwenden, um die Leistung zu verbessern, haben Sie keine Kontrolle über die Reihenfolge, in der die Elemente durch forEach zur Ausgabeliste hinzugefügt werden. 

Wenn Sie toList verwenden, behält die Streams-API die Reihenfolge bei, selbst wenn Sie einen parallelen Stream verwenden.

1
Eran

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

annahme List<String>

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

mit einem Micro-Micro-Benchmark, 1M-Einträgen, 20% Nullen und einfacher 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}

parallele ohne Nullen und Filter (so dass der Stream SIZED ist): toArrays hat in diesem Fall die beste Leistung und .forEach() schlägt mit "indexOutOfBounds" in der empfangenen ArrayList fehl und musste mit .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}
0
harshtuna

Möglicherweise Methode 3.

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());
0
Kumar Abhishek

Wenn 3. Pary Libaries verwendet wird, ist ok cyclops -rea definiert 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 erneut verwendet wird).

[Offenlegung Ich bin der Hauptentwickler von Cyclops-React]

0
John McClean