193

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

4

8 に答える 8

234

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

短い答えはnoです。少なくとも、一般的には、 a を使用し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))

もちろん、毎回これを行うことを忘れないでしょう?:-) そうだとしましょう。次に、パフォーマンス チームは、慎重に作成されたすべての並列実装がスピードアップを提供しない理由を疑問に思うでしょう。そしてもう一度、ストリーム全体を順番に実行するように強制しているコードまで追跡します。

やらないでください。

于 2014-03-31T07:40:23.023 に答える
202

私が見る限り、これまでの他のすべての回答では、コレクターを使用して要素を既存のストリームに追加していました。ただし、より短い解決策があり、順次ストリームと並列ストリームの両方で機能します。メソッド参照と組み合わせてforEachOrderedメソッドを使用するだけです。

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

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

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

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

于 2014-03-31T06:53:52.100 に答える
13

簡単に言えば、いいえです (または、いいえである必要があります)。編集:ええ、可能です(以下のassyliasの回答を参照)が、読み続けてください。EDIT2:しかし、まだそれをすべきではない別の理由については、Stuart Marksの回答を参照してください!

より長い答え:

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

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

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のイントロ/本を実際に見つけて読む努力を含める場合)

既存のリストを変更することが一般的に悪い考えであり、コードの保守性の問題の範囲外であるローカル変数を変更し、アルゴリズムが短くて簡単でない限り、なぜ保守しにくいコードになるのかを調べること—関数型プログラミングの優れた入門書を見つけて (数百あります)、読み始めます。「プレビュー」の説明は次のようなものです: より数学的に健全であり、(プログラムのほとんどの部分で) データを変更しないことを推論するのが簡単であり、より高度で技術的ではない (そして、脳が一度理解すれば、より人間に優しい) ことにつながります。古いスタイルの命令的思考からの移行) プログラム ロジックの定義。

于 2014-03-31T05:04:52.707 に答える
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())
);

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

于 2014-03-31T04:51:50.783 に答える
0

既存のリストがあり、このアクティビティに Java 8 を使用するとします `

import java.util.*;
import java.util.stream.Collectors;

public class AddingArray {

    public void addArrayInList(){
        List<Integer> list = Arrays.asList(3, 7, 9);

   // And we have an array of Integer type 

        int nums[] = {4, 6, 7};

   //Now lets add them all in list
   // converting array to a list through stream and adding that list to previous list
        list.addAll(Arrays.stream(nums).map(num -> 
                                       num).boxed().collect(Collectors.toList()));
     }
}

`

于 2020-08-15T15:37:17.237 に答える