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());
Machen Sie sich keine Sorgen über Leistungsunterschiede, in diesem Fall sind sie normalerweise minimal.
Methode 2 ist vorzuziehen, weil
es ist nicht erforderlich, eine Sammlung zu mutieren, die außerhalb des Lambda-Ausdrucks existiert.
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 )
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.
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());
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
ArrayList
).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.
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.
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.
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}
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());
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]