回答:
注: nosidの回答は、を使用して既存のコレクションに追加する方法を示していますforEachOrdered()
。これは、既存のコレクションを変更するための便利で効果的な手法です。私の答えはCollector
、既存のコレクションを変更するためにを使用してはならない理由を説明しています。
短い答えはノー、少なくとも一般的にはノーCollector
です。既存のコレクションを変更するためにを使用するべきではありません。
その理由は、コレクターは、スレッドセーフではないコレクションに対しても、並列処理をサポートするように設計されているためです。これを行う方法は、中間結果の独自のコレクションで各スレッドが独立して動作するようにすることです。各スレッドが独自のコレクションを取得する方法は、毎回新しいコレクションCollector.supplier()
を返すために必要なを呼び出すことです。
中間結果のこれらのコレクションは、単一の結果コレクションになるまで、再びスレッド限定の方法でマージされます。これがcollect()
操作の最終結果です。
Balderとassyliasからのいくつかの回答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))
もちろん、毎回これを忘れないでくださいね。:-)あなたがそうだとしましょう。次に、パフォーマンスチームは、慎重に作成されたすべての並列実装がなぜ高速化を提供していないのか疑問に思います。そしてもう一度彼らはそれをあなたのコードまでたどり、ストリーム全体を順番に実行させます。
しないでください。
toCollection
メソッドのサプライヤ引数が毎回新しい空のコレクションを返さなければならないことを要求するという事実は、私にそうしないように説得します。コアJavaクラスのjavadocコントラクトを破りたいです。
forEachOrdered
。副作用には、すでに要素があるかどうかに関係なく、既存のコレクションに要素を追加することが含まれます。ストリームの要素を新しいコレクションに配置する場合は、collect(Collectors.toList())
or toSet()
またはを使用しますtoCollection()
。
私の知る限り、これまでの他のすべての回答では、コレクターを使用して既存のストリームに要素を追加していました。ただし、より短いソリューションがあり、シーケンシャルストリームとパラレルストリームの両方で機能します。メソッド参照と組み合わせて、メソッドforEachOrderedを使用するだけです。
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
唯一の制限は、ソースとターゲットが異なるリストであることです。これは、ストリームが処理されている限り、ソースに変更を加えることができないためです。
このソリューションはシーケンシャルストリームとパラレルストリームの両方で機能することに注意してください。ただし、同時実行によるメリットはありません。forEachOrderedに渡されたメソッド参照は、常に順次実行されます。
forEachOrdered
代わりに使用した理由はありますforEach
か?
forEachOrdered
両方で機能します。対照的に、並列ストリームでは、渡された関数オブジェクトを同時に実行する可能性があります。この場合、関数オブジェクトは、たとえばを使用して適切に同期する必要があります。forEach
Vector<Integer>
target::add
。メソッドが呼び出されるスレッドに関係なく、データの競合はありません。私はあなたがそれを知っていると思っていただろう。
簡単な答えは「いいえ」です(または「いいえ」である必要があります)。編集:ええ、それは可能ですが(以下の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イントロ/ブックを見つけて読む努力を含める場合)
既存のリストを変更することは一般に悪い考えであり、ローカル変数を変更していて、アルゴリズムが短く、かつ/または自明でない限り、コードの保守性の問題の範囲外である場合を除き、コードの保守性が低下する理由を見つけるため—関数型プログラミング(数百に及ぶ)の適切な紹介を見つけて、読み始めます。「プレビュー」の説明は、次のようなものになります。データが(プログラムのほとんどの部分で)変更されないほうが数学的に健全であり、推論が容易であり、レベルが高く、技術的ではありません(また、脳が人にやさしい)プログラムロジックの古いスタイルの命令的思考)定義からの移行。
エリック・アリクすでに非常に適切な理由を述べています。これは、ストリームの要素を既存のリストに収集したくない場合がほとんどである理由です。
とにかく、この機能が本当に必要な場合は、次のワンライナーを使用できます。
しかし、Stuart Marksが彼の答えで説明しているように、ストリームが並列ストリームである可能性がある場合は、これを行うべきではありません-自己責任で使用してください...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
元のリストを参照して、そのリストを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())
);
これが、この関数型プログラミングパラダイムが提供するものです。
古いリストと新しいリストをストリームとして連結し、結果を宛先リストに保存します。並行してうまく機能します。
スチュアートマークスの回答例を使用します。
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]
それが役に立てば幸い。
Collection
」