Java8ストリームの要素を既存のリストに追加する方法


回答:


198

注: nosidの回答は、を使用して既存のコレクションに追加する方法を示していますforEachOrdered()。これは、既存のコレクションを変更するための便利で効果的な手法です。私の答えはCollector、既存のコレクションを変更するためにを使用してはならない理由を説明しています。

短い答えはノー、少なくとも一般的にはノーCollectorです。既存のコレクションを変更するためにを使用するべきではありません。

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

中間結果のこれらのコレクションは、単一の結果コレクションになるまで、再びスレッド限定の方法でマージされます。これがcollect()操作の最終結果です。

Balderassyliasからのいくつかの回答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、スレッドセーフでないデータ構造である複数のスレッドが動作しているためです。OK、同期させましょう:

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))

とにかく。これをしないことをお勧めします。ストリームを制御している場合は、それが並行して実行されないことを保証できます。コレクションではなくストリームが渡されるプログラミングのスタイルが出現すると思います。誰かがあなたにストリームを渡してこのコードを使用した場合、ストリームがたまたま並列であると失敗します。さらに悪いことに、誰かがシーケンシャルストリームを渡した場合、このコードはしばらくの間正常に動作し、すべてのテストに合格するなどします。その後、任意の時間後に、システムの他の場所のコードが、並列ストリームを使用するように変更されコードが発生する可能性があります壊す。

わかりました。sequential()このコードを使用する前に、必ずストリームを呼び出すことを忘れないでください。

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

もちろん、毎回これを忘れないでくださいね。:-)あなたがそうだとしましょう。次に、パフォーマンスチームは、慎重に作成されたすべての並列実装がなぜ高速化を提供していないのか疑問に思います。そしてもう一度彼らはそれをあなたのコードまでたどり、ストリーム全体を順番に実行させます

しないでください。


素晴らしい説明!-これを明確にしてくれてありがとう。可能性のある並列ストリームでこれを実行しないことを推奨するように、私の回答を編集します。
Balder 2014年

3
質問がある場合、既存のリストにストリームの要素を追加するワンライナーがある場合、簡単な答えはyesです。私の答えを見てください。ただし、Collectors.toCollection()を既存のリストと組み合わせて使用​​するのは間違った方法であることに同意します。
nosid 2014年

そうだね。私たちの残りはすべてコレクターのことを考えていたと思います。
スチュアートマークス

正解です。前述のとおり、適切に機能する必要があるため、明確にアドバイスしない場合でも、シーケンシャルソリューションを使用したくなります。しかし、Javadocは、toCollectionメソッドのサプライヤ引数が毎回新しい空のコレクションを返さなければならないことを要求するという事実は、私にそうしないように説得します。コアJavaクラスのjavadocコントラクトを破りたいです。
2016年

1
@AlexCurversストリームに副作用を持たせたい場合は、ほぼ確実にを使用する必要がありますforEachOrdered。副作用には、すでに要素があるかどうかに関係なく、既存のコレクションに要素を追加することが含まれます。ストリームの要素を新しいコレクションに配置する場合は、collect(Collectors.toList())or toSet()またはを使用しますtoCollection()
スチュアートマークス

169

私の知る限り、これまでの他のすべての回答では、コレクターを使用して既存のストリームに要素を追加していました。ただし、より短いソリューションがあり、シーケンシャルストリームとパラレルストリームの両方で機能します。メソッド参照と組み合わせて、メソッドforEachOrderedを使用するだけです。

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

唯一の制限は、ソースターゲットが異なるリストであることです。これは、ストリームが処理されている限り、ソースに変更を加えることができないためです。

このソリューションはシーケンシャルストリームとパラレルストリームの両方で機能することに注意してください。ただし、同時実行によるメリットはありません。forEachOrderedに渡されたメソッド参照は、常に順次実行されます。


6
+1非常に多くの人が、存在する可能性はないと主張しているのは面白いです。ところで 2か月前の回答にforEach(existing::add)可能性として含めました。私も追加すべきだった…forEachOrdered
ホルガー

5
forEachOrdered代わりに使用した理由はありますforEachか?
メンバーサウンド2015年

6
@membersound:シーケンシャルストリームとパラレルストリームのforEachOrdered両方で機能ます。対照的に、並列ストリームでは、渡された関数オブジェクトを同時に実行する可能性があります。この場合、関数オブジェクトは、たとえばを使用して適切に同期する必要があります。forEachVector<Integer>
nosid 2015年

@BrianGoetz:私はの文書化することを、認めざるを得ないStream.forEachOrderedはビット不正確です。ただし、この仕様の合理的な解釈はわかりません。つまり、の2つの呼び出しの間に前に発生する関係はありませんtarget::add。メソッドが呼び出されるスレッドに関係なく、データの競合はありません。私はあなたがそれを知っていると思っていただろう。
nosid 2015

これは、私にとっては、最も有用な答えです。実際には、ストリームから既存のリストにアイテムを挿入する実際的な方法を示しています。これは、質問の質問です(誤解を招く単語「collect」にもかかわらず)
Wheezil

12

簡単な答えは「いいえ」です(または「いいえ」である必要があります)。編集:ええ、それは可能ですが(以下のassyliasの回答を参照)、読み続けてください。EDIT2:しかし、スチュアートマークスの回答を参照してくださいこれは、まだやるべきではないもう1つの理由です。

より長い答え:

Java 8のこれらの構成体の目的は、関数型プログラミングのいくつかの概念を言語に導入することです。関数型プログラミングでは、データ構造は通常は変更されません。代わりに、マップ、フィルター、フォールド/リデュースなどの変換によって、古いデータ構造から新しいデータ構造が作成されます。

古いリストを変更する必要がある場合は、マップされたアイテムを新しいリストに収集するだけです。

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

そして、そうしますlist.addAll(newList)—もう一度:あなたが本当にしなければならない場合。

(または、古いリストと新しいリストを連結した新しいリストを作成し、それをlist変数に割り当てます。これは、FPよりも少し精神的ですaddAll

APIについて:APIで許可されていても(ここでも、assyliasの回答を参照)、少なくとも一般的には、関係なくそうすることは避けてください。パラダイム(FP)と闘わずにそれを学ぼうとするのではなく(Javaは一般にFP言語ではありませんが)、絶対に必要な場合にのみ「ダーティ」な戦術に頼るのが最善です。

本当に長い答え:(つまり、提案されているように実際にFPイントロ/ブックを見つけて読む努力を含める場合)

既存のリストを変更することは一般に悪い考えであり、ローカル変数を変更していて、アルゴリズムが短く、かつ/または自明でない限り、コードの保守性の問題の範囲外である場合を除き、コードの保守性が低下する理由を見つけるため—関数型プログラミング(数百に及ぶ)の適切な紹介を見つけて、読み始めます。「プレビュー」の説明は、次のようなものになります。データが(プログラムのほとんどの部分で)変更されないほうが数学的に健全であり、推論が容易であり、レベルが高く、技術的ではありません(また、脳が人にやさしい)プログラムロジックの古いスタイルの命令的思考)定義からの移行。


@assylias:論理的には、または部分があったので問題ありませんでした。とにかく、メモを追加しました。
エリックカプルン2014年

1
短い答えは正しいです。提案されたワンライナーは、単純なケースでは成功しますが、一般的なケースでは失敗します。
スチュアートマークス

長い方の答えはほぼ正しいですが、APIの設計は主に並列処理に関するものであり、関数型プログラミングに関するものではありません。もちろん、FPには並列処理が可能なものがたくさんあるので、これら2つの概念はよく整合しています。
スチュアートマークス

@StuartMarks:興味深い:アッシリアの回答で提供されるソリューションはどのケースで失敗しますか?(そして並列処理に関する良い点—私はFPを擁護することに熱心すぎたと思います)
Erik Kaplun 2014年

@ErikAllikこの問題をカバーする回答を追加しました。
スチュアートマークス

11

エリック・アリクすでに非常に適切な理由を述べています。これは、ストリームの要素を既存のリストに収集したくない場合がほとんどである理由です。

とにかく、この機能が本当に必要な場合は、次のワンライナーを使用できます。

しかし、Stuart Marksが彼の答えで説明しているように、ストリームが並列ストリームである可能性がある場合は、これを行うべきではありません-自己責任で使用してください...

list.stream().collect(Collectors.toCollection(() -> myExistingList));

ああ、それは残念だ:P
エリック・カプルーン2014年

2
ストリームが並行して実行されている場合、この手法はひどく失敗します。
スチュアートマークス

1
失敗しないことを確認するのは、コレクションプロバイダーの責任です。たとえば、同時コレクションを提供するなどです。
Balder 2014年

2
いいえ、このコードはtoCollection()の要件に違反しています。これは、サプライヤーが適切なタイプの新しい空のコレクションを返すということです。宛先がスレッドセーフであっても、並列処理で行われるマージは誤った結果を引き起こします。
スチュアートマークス

1
@バルダー私はこれを明確にするはずの答えを追加しました。
スチュアートマークス

4

元のリストを参照して、そのリストをCollectors.toList()返す必要があります。

ここにデモがあります:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

新しく作成した要素を1行で元のリストに追加する方法は次のとおりです。

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

これが、この関数型プログラミングパラダイムが提供するものです。


単に再割り当てするのではなく、既存のリストに追加/収集する方法を言うつもりでした。
codefx 2014年

1
技術的には、関数型プログラミングのパラダイムではそのようなことを行うことはできません。これはストリームがすべてです。関数型プログラミングでは、状態は変更されません。代わりに、永続的なデータ構造に新しい状態が作成され、同時実行の目的で安全になり、より機能的になります。私が述べたアプローチはあなたができることです、またはあなたは古いスタイルのオブジェクト指向のアプローチに頼ることができ、各要素を繰り返し、そしてあなたが適切と思うように要素を維持または削除します。
Aman Agnihotri 2014年

0

targetList = sourceList.stream()。flatmap(List :: stream).collect(Collectors.toList());


0

古いリストと新しいリストをストリームとして連結し、結果を宛先リストに保存します。並行してうまく機能します。

スチュアートマークスの回答例を使用します。

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

それが役に立てば幸い。

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