web-dev-qa-db-ja.com

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

CollectorのJavadoc は、ストリームの要素を新しいListに集める方法を示しています。既存のArrayListに結果を追加するワンライナーはありますか?

129
codefx

NOTE:nosid's answerforEachOrdered()を使って既存のコレクションに追加する方法を示しています。これは既存のコレクションを変更するための便利で効果的な手法です。私の答えは、なぜあなたが既存のコレクションを変更するためにCollectorを使うべきではないかを述べています。

簡単な答えはnoです。少なくとも一般的ではありませんが、既存のコレクションを変更するためにCollectorを使用しないでください。

その理由は、スレッドセーフではないコレクションに対しても、コレクターが並列処理をサポートするように設計されているためです。これを行う方法は、各スレッドに独自の中間結果の集合を個別に処理させることです。各スレッドが独自のコレクションを取得する方法は、毎回newコレクションを返すために必要な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で複数のスレッドが動作しているためです。それでは、同期させましょう。

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]

これは、上で説明したスレッド限定の累積/マージ操作の結果です。パラレルストリームでは、各スレッドはサプライヤを呼び出して、中間蓄積用の独自のコレクションを取得します。 sameコレクションを返すサプライヤを渡すと、各スレッドはその結果をそのコレクションに追加します。スレッド間の順序付けはないため、結果は任意の順序で追加されます。

次に、これらの中間コレクションがマージされると、基本的にリストとそれ自体がマージされます。リストはList.addAll()を使用してマージされます。つまり、操作中にソースコレクションが変更された場合、結果は未定義になります。この場合、ArrayList.addAll()は配列コピー操作を行うので、それ自体が複製されることになります。これは、予想されることの一種です。とにかく、これは宛先の中で奇妙な結果と重複した要素を説明しています。

「ストリームを順番に実行するようにしてください」と言って、先に進んで次のようなコードを書いてください。

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

とにかく。私はこれをしないようにお勧めします。ストリームを制御すれば、確かに、それが並行して実行されないことを保証できます。コレクションの代わりにストリームが流されるところでは、プログラミングのスタイルが現れると期待しています。誰かがあなたにストリームを渡して、あなたがこのコードを使うならば、ストリームが偶然並列であるならばそれは失敗するでしょう。さらに悪いことに、誰かがあなたにシーケンシャルストリームを渡し、このコードはしばらくの間うまくいくでしょう、すべてのテストに合格するなど。その後、しばらくして、システム内のどこかのコードが並列ストリームを使うように変わるかもしれません。 your破るコード。

それでは、このコードを使用する前に、必ず任意のストリームでsequential()を呼び出すようにしてください。

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

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

しないでください。

150
Stuart Marks

私の知る限りでは、他のすべての答えはこれまでのところコレクターを使って既存のストリームに要素を追加していました。しかし、もっと短い解決方法があり、それはシーケンシャルストリームとパラレルストリームの両方に有効です。メソッド参照と組み合わせて、メソッドforEachOrderedを使用するだけです。

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

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

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

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

143
nosid

簡単な答えはnoです(またはnoであるべきです)。 編集:ええ、それは可能です(以下のassyliasの答えを見てください)が、読み続けてください。 EDIT2:でもスチュアート・マークスの答えを見てください。

長い答え:

Java 8におけるこれらの構成要素の目的は、 関数型プログラミング という概念を言語に導入することです。関数型プログラミングでは、データ構造は通常は変更されず、代わりに、マップ、フィルタ、折りたたみ/縮小などの変換を使用して古いものから新しいものが作成されます。

あなたがmust古いリストを修正するならば、単純に新しいリストにマッピングされた項目を集めてください:

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

それからlist.addAll(newList)をもう一度実行してください - 本当に必要な場合。

(または、古いリストと新しいリストを連結して新しいリストを作成し、それをlist変数に代入します。これは、addAllよりもFPのほうが少しです。 )

APIに関しては:たとえAPIがそれを許可していたとしても(やはり、assyliasの答えを見てください)、少なくとも一般的に言って、そうすることを避けようとするべきです。 (Javaは一般にFP言語ではないにもかかわらず)パラダイム(FP)と戦わずに、それを学ぶことを試みることをお勧めします。

本当に長い答え:(つまり、FP紹介/本を実際に見つけて読むという努力が含まれている場合)

既存のリストを変更することが一般的に悪い考えであり、コードの保守性の問題の範囲外であるローカル変数を変更したり、アルゴリズムが短くても自明でない場合を除いて、保守性の低いコードにつながる理由 - 関数型プログラミングの入門書(何百もあります)を見つけて、読み始めてください。 「プレビュー」の説明は次のようになります。(プログラムのほとんどの部分で)データが変更されないようにする方が数学的に健全であり、簡単で、高レベルで技術的ではありません。プログラムロジックの定義(昔ながらの命令的な考え方)から移行します。

11
Erik Allik

Erik Allik すでに非常に正当な理由が出ています。なぜならストリームの要素を既存のListに集めたくないのでしょう。

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

しかし、 Stuart Marks が彼の答えで説明しているように、ストリームがパラレルストリームである可能性がある場合は、絶対にしないでください。あなた自身のリスク...

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

あなたは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())
);

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

4
Aman Agnihotri