私はリストmyListToParse
を持っていて、ここで私は要素をフィルタして各要素にメソッドを適用し、そして結果を別のリストmyFinalList
に追加します。
Java 8では、2つの方法で実行できることに気付きました。私はそれらの間のより効率的な方法を知り、そして一方の方法が他方の方法より優れている理由を理解したいと思います。
私は3番目の方法についての提案を開いています。
方法1:
myFinalList = new ArrayList<>();
myListToParse.stream()
.filter(elt -> elt != null)
.forEach(elt -> myFinalList.add(doSomething(elt)));
方法2:
myFinalList = myListToParse.stream()
.filter(elt -> elt != null)
.map(elt -> doSomething(elt))
.collect(Collectors.toList());
パフォーマンスの違いを気にする必要はありません。通常、この場合は最小限になります。
方法2が好ましいのは、
ラムダ式の外側に存在するコレクションを変更する必要はありません。
コレクションパイプラインで実行されるさまざまな手順が順番に(最初にフィルター操作、次にマップ操作、そして結果の収集)書き込まれるため、読みやすくなります(コレクションパイプラインの利点の詳細については、Martin Fowler( 優れた記事 )
使用されているCollector
を置き換えることで、値の収集方法を簡単に変更できます。場合によっては、独自のCollector
を書く必要があるかもしれませんが、その場合の利点はそれを簡単に再利用できることです。
2番目の形式は副作用がなく、並列化が容易であるため、2番目の形式の方が優れているという既存の回答に同意します(単に並列ストリームを使用する)。
パフォーマンス面では、並列ストリームを使い始めるまでは同等のようです。その場合、mapの方がはるかに良いパフォーマンスを発揮します。以下の マイクロベンチマーク の結果を参照してください。
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
forEachは端末メソッドであるため、最初の例を同じ方法で拡張することはできません。つまり、voidが返されます。しかし 並列ストリームを使用している場合は これは本当に悪い考えです。
最後に、2番目のスニペットは、メソッド参照と静的インポートを使って、より簡潔に書くことができます。
myFinalList = myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.collect(toList());
ストリームを使用することの主な利点の1つは、宣言的な方法で、つまり関数型のプログラミングを使用してデータを処理できることです。それはまたあなたのストリームを並行にするために特別なマルチスレッドコードを書く必要がないという自由な意味のためのマルチスレッド機能を与えます。
このスタイルのプログラミングを検討している理由は、これらの利点を活用したいということであると仮定すると、最初のコードサンプルは潜在的に機能しません(foreach
メソッドは終了として分類されます)。
2番目の方法は、マップ関数がステートレスラムダ関数を受け入れることができるので、関数型プログラミングの観点からは好まれます。もっと明確に言うと、map関数に渡されるラムダは
ArrayList
)。2番目の方法のもう1つの利点は、ストリームが並列で、コレクターが並行で順序付けされていない場合、これらの特性が、並行して収集を行うための削減操作に役立つヒントを提供することです。
あなたが Eclipseコレクション を使うなら、collectIf()
メソッドを使うことができます。
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);
それは熱心に評価され、Streamを使うより少し速いはずです。
注:私はEclipseコレクションのコミッターです。
私は2番目の方法を好みます。
最初の方法を使用するときに、パフォーマンスを向上させるために並列ストリームを使用することにした場合、forEach
によって要素が出力リストに追加される順序を制御できません。
toList
を使用すると、パラレルストリームを使用している場合でもStreams APIは順序を保持します。
stream().toArray()
を使用する - の下のコメントを参照してください。なぜtoListメソッドをストリームできないのか を参照してください。 forEach()やcollect()より遅くなり、表現力が落ちます。将来のJDKビルドで最適化される可能性があるため、念のためここで追加します。
List<String>
を仮定する
myFinalList = Arrays.asList(
myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.toArray(String[]::new)
);
マイクロ・マイクロベンチマーク、1Mエントリ、20%ヌル、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;
}
結果は
平行:
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}
シーケンシャル:
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}
nullとフィルタなしの並列処理(したがってストリームはSIZED
):そのような場合にtoArraysが最高のパフォーマンスを発揮し、.forEach()
は受信側ArrayListの "indexOutOfBounds"で失敗し、.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}
方法3かもしれません。
私はいつも論理を分けておくことを好む。
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());
3rd Pary Librariesを使用しても問題ない場合 cyclops-react は、この機能を組み込んでLazy拡張コレクションを定義します。
ListX myListToParse;
ListX myFinalList = myListToParse.filter(elt - > elt!= null).map(elt - > doSomething(elt));
myFinalListは、最初のアクセスまで(そしてマテリアライズド・リストがキャッシュに入れられて再利用されるまで)評価されません。
[私はcyclops-reactの主導的開発者です]