Java 8-リストを変換する最良の方法:マップまたはforeach?


188

私は、リスト持ってmyListToParse、私は要素をフィルタリングし、各要素に対してメソッドを適用して、別のリストに結果を追加したいですmyFinalList

Java 8では、2つの方法で実行できることに気付きました。それらの間のより効率的な方法を知り、なぜ一方が他方よりも優れているのかを理解したいと思います。

私は、第三の方法についての提案をお待ちしています。

方法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()); 

55
二つ目。適切な関数には副作用があってはならず、最初の実装では外部の世界を変更します。
ThanksForAllTheFish 2015

37
単なるスタイルの問題ですelt -> elt != nullが、置き換えることができますObjects::nonNull
the8472

2
@ the8472さらに良いのは、最初にコレクションにnull値がないことを確認し、Optional<T>代わりにと組み合わせて使用することflatMapです。
ハーマン

2
@SzymonRoziewski、かなりではありません。これと同じくらい簡単なものの場合、フードの下で並列ストリームをセットアップするために必要な作業は、この構成のミュートを使用することになります。
MK

2
.map(this::doSomething)これdoSomethingは非静的メソッドであると想定して記述できることに注意してください。静的な場合はthis、クラス名に置き換えることができます。
ヘルマン

回答:


153

パフォーマンスの違いを心配する必要はありません。この場合、通常は最小限に抑えられます。

方法2は、

  1. ラムダ式の外側にあるコレクションを変更する必要はありません。

  2. コレクションパイプラインで実行されるさまざまな手順が順番に記述されるため、読みやすくなります。最初にフィルター操作、次にマップ操作、そして結果を収集します(コレクションパイプラインの利点の詳細については、Martin Fowlerの優れた記事を参照してください)。

  3. Collector使用されるを置き換えることで、値の収集方法を簡単に変更できます。独自のを作成する必要がある場合もありますCollectorが、その利点は簡単に再利用できることです。


43

2番目の形式の方が副作用がなく、並列化(並列ストリームを使用するだけ)が容易であるため、2番目の形式の方が良いという既存の回答に同意します。

パフォーマンスに関しては、並列ストリームの使用を開始するまでは同等であるように見えます。その場合、マップのパフォーマンスは非常に良くなります。以下のマイクロベンチマークの結果を参照してください。

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」よりも実際に優先されます。ミリ秒単位の私のベンチマーク:SO28319064.forEach:187,310±1,768 ms / op-SO28319064.map:189,180±1,692 ms / op --SO28319064.mapParallelStream:55,577±0,782 ms / op
Giuseppe Bertone

2
@GiuseppeBertone、それはassylias次第ですが、私の意見ではあなたの編集は元の作者の意図と矛盾しています。独自の回答を追加したい場合は、既存の回答をそれほど編集するのではなく、追加することをお勧めします。また、マイクロベンチマークへのリンクは結果に関連しなくなりました。
Tagir Valeev 2016年

5

ストリームを使用する主な利点の1つは、宣言的な方法でデータを処理できること、つまり関数型のプログラミングを使用できることです。また、マルチスレッド機能を無料で提供するため、ストリームを並行させるために追加のマルチスレッドコードを記述する必要はありません。

このプログラミングスタイルを検討している理由は、これらの利点を活用するためであり、最初のコードサンプルは、foreachメソッドがターミナルとして分類されている(つまり、副作用をもたらす可能性がある)ため、機能しない可能性があります。

2つ目の方法は、map関数がステートレスラムダ関数を受け入れることができるため、関数型プログラミングの観点から推奨されます。より明示的には、マップ関数に渡されるラムダは

  1. 非干渉。つまり、関数がストリームのソースを変更しないことを意味します(たとえば、ArrayList)。
  2. 並列処理の実行時に予期しない結果を回避するためのステートレス(スレッドスケジューリングの違いが原因)。

2番目のアプローチのもう1つの利点は、ストリームが並列であり、コレクターが並行かつ順序付けされていない場合、これらの特性は、削減を同時に実行するための削減操作に役立つヒントを提供できることです。


4

あなたが使用している場合は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コレクションのコミッターです。


1

私は2番目の方法を好みます。

最初の方法を使用する場合、パフォーマンスを向上させるために並列ストリームを使用することを決定すると、要素がによって出力リストに追加される順序を制御できなくなりますforEach

を使用するtoListと、Streams APIは、並列ストリームを使用する場合でも順序を保持します。


これが正しいアドバイスであるかどうかはわかりません。並列ストリームを使用したいが、順序を維持したい場合は、forEachOrdered代わりに使用できますforEach。ただし、forEach状態のドキュメントとして、遭遇順序を維持すると並列処理の利点が犠牲になります。そのときもそうだと思いtoListます。
ヘルマン

0

3番目のオプションがあります-使用stream().toArray()- ストリームにtoListメソッドがなかった理由のコメントを参照してください。これは、forEach()またはcollect()よりも遅く、表現力が低いことがわかります。今後のJDKビルドで最適化される可能性があるため、念のためここに追加します。

想定 List<String>

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

micro-microベンチマーク、1Mエントリ、20%null、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}

0

方法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());

0

3rd Pary Libariesを使用しても問題がない場合、cyclops-reactは、この機能が組み込まれたLazy拡張コレクションを定義します。たとえば、次のように簡単に記述できます。

ListX myListToParse;

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

myFinalListは、最初のアクセスまで(そしてマテリアライズされたリストがキャッシュされて再利用された後)評価されません。

[開示私はc​​yclops-reactの主要開発者です]

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.