1. ホーム
  2. java

[解決済み] Java8ストリームの要素を既存のListに追加する方法

2022-04-13 11:25:17

質問

コレクタのJavadoc は、ストリームの要素を新しいリストに収集する方法を示しています。既存のArrayListに結果を追加するワンライナーはないのでしょうか?

どのように解決するのですか?

注意事項 nosidの回答 を使用して、既存のコレクションに追加する方法を示しています。 forEachOrdered() . これは、既存のコレクションを変異させるための便利で効果的なテクニックです。私の答えは、なぜ Collector を使用して、既存のコレクションを変更することができます。

簡単に言うと いいえ 少なくとも、一般的には Collector を使用して、既存のコレクションを変更することができます。

コレクターは、スレッドセーフでないコレクションに対しても、並列処理をサポートするように設計されているからだ。その方法は、各スレッドが中間結果のコレクションに対して独立して動作するようにすることである。各スレッドが独自のコレクションを取得する方法は、スレッドセーフのために Collector.supplier() を返すことが要求される。 新しい コレクションを毎回作成します。

これらの中間結果のコレクションは、1つの結果のコレクションになるまで、再びスレッドで制限された方法でマージされる。これが collect() 演算を行う。

からの回答がいくつかあります。 バルダー アシリャス を使用することを提案しています。 Collectors.toCollection() で、新しいリストではなく、既存のリストを返すサプライヤーを渡します。これは、毎回新しい空のコレクションを返すというサプライヤの要件に違反する。

これは、彼らの回答にある例が示すように、単純なケースではうまくいくでしょう。しかし、特にストリームが並列に実行されている場合は、失敗するでしょう。(ライブラリの将来のバージョンでは、予期せぬ方法で変更され、順次実行の場合であっても失敗する可能性があります)。

簡単な例で説明しましょう。

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

このプログラムを実行すると、よく ArrayIndexOutOfBoundsException . に対して複数のスレッドが動作しているためです。 ArrayList というスレッドセーフでないデータ構造です。よし、同期化しよう。

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

これで、例外が発生して失敗することはなくなりました。しかし、期待通りの結果ではなく

[foo, 0, 1, 2, 3]

を実行すると、次のような変な結果になります。

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

これは、上で説明したスレッドに限定した蓄積・合算操作の結果です。並列ストリームでは、各スレッドがサプライヤーを呼び出して、中間蓄積のための独自のコレクションを取得します。を返すサプライヤを渡すと、そのサプライヤは 同じ コレクションを作成すると、各スレッドはその結果をそのコレクションに追加します。スレッド間の順序付けがないので、結果は任意の順序で追加されることになる。

そして、これらの中間コレクションがマージされるとき、これは基本的にリストと自分自身をマージする。リストのマージは List.addAll() これは、操作中にソース・コレクションが変更された場合、結果が不定になることを意味します。この場合 ArrayList.addAll() は配列のコピー操作を行うので、結局は自分自身を複製することになり、これはある意味予想されたことだと思います。(他のListの実装では全く異なる挙動をする可能性があることに注意してください)。いずれにせよ、これで出力先での奇妙な結果や重複する要素の説明がつきます。

ストリームをシーケンシャルに実行するようにしよう」といって、次のようなコードを書くかもしれません。

stream.collect(Collectors.toCollection(() -> existingList))

のように、とにかく このようなことはしないことをお勧めします。ストリームを制御すれば、確かに、並列に実行されないことを保証できます。今後は、コレクションの代わりにストリームを渡すようなプログラミングのスタイルが出現すると思うんだ。ストリームを渡されて、このコードを使っても、そのストリームがたまたま並列であった場合は失敗します。さらに悪いことに、誰かがあなたに順次的なストリームを渡すかもしれない。そして、ある任意の時間後に、システムの他の場所でコードが並列ストリームを使うように変更されるかもしれません。 あなたの のコードが壊れる。

OK、では忘れずに sequential() を、このコードを使用する前に任意のストリーム上で実行します。

stream.sequential().collect(Collectors.toCollection(() -> existingList))

もちろん、毎回忘れずに行いますよね:-) 仮にそうしたとしましょう。そうすると、パフォーマンス・チームは、慎重に作られた並列実装がなぜスピードアップしないのか不思議に思うでしょう。そしてまた、その原因を突き止めるのです。 あなたの のコードで、ストリーム全体を順次実行するように強制しています。

やめておけよ。