Java 8ストリーム-収集と削減


143

いつcollect()vs を使用しreduce()ますか?誰かがどちらか一方に行く方が間違いなく良いときの良い具体的な例がありますか?

Javadocは、collect()は変更可能な縮小であると述べています

これは変更可能な削減であることを考えると、同期が(内部で)必要であり、その結果、パフォーマンスが低下する可能性があります。おそらくreduce()、reduceの各ステップの後に返すために新しいデータ構造を作成する必要があるという犠牲を払って、より容易に並列化できます。

上記のステートメントは当て推量ですが、ここでチャイムを鳴らす専門家が大好きです。


1
あなたがリンクしたページの残りの部分はそれを説明しています:reduce()と同様に、この抽象的な方法で収集を表現する利点は、それが並列化に直接影響を受けやすいことです:部分的な結果を並列に蓄積し、それらを組み合わせることができる限り、累積関数と結合関数は適切な要件を満たします。
JBニゼット2014年

1
-アンジェリカランガーによって:また、「コレクト対削減のJava 8でストリーム」を参照してください。 youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

回答:


115

reducefold」演算であり、ストリームの各要素に2項演算子を適用します。演算子の最初の引数は前のアプリケーションの戻り値で、2番目の引数は現在のストリーム要素です。

collect「コレクション」が作成され、各要素がそのコレクションに「追加」される集約操作です。次に、ストリームの異なる部分にあるコレクションが一緒に追加されます。

あなたがリンクされたドキュメントは、 2つの異なるアプローチを持つための理由を与えます:

文字列のストリームを受け取り、それらを単一の長い文字列に連結したい場合は、通常の削減でこれを実現できます。

 String concatenated = strings.reduce("", String::concat)  

望ましい結果が得られ、それは並行して機能します。ただし、パフォーマンスに満足できない場合があります。このような実装では文字列のコピーが大量に行われ、実行時間は文字数でO(n ^ 2)になります。よりパフォーマンスの高いアプローチは、文字列を蓄積するための可変コンテナであるStringBuilderに結果を蓄積することです。通常の削減と同じ手法を使用して、可変削減を並列化できます。

つまり、ポイントはどちらの場合も並列化は同じであるということですがreduce、ストリーム要素自体に関数を適用する場合です。collect変更可能なコンテナに関数を適用する場合。


1
これがcollectの場合である場合:「よりパフォーマンスの高い方法は、結果をStringBuilderに蓄積することです」ならば、なぜ削減を使用するのでしょうか。
jimhooker2002 14年

2
@ Jimhooker2002が再読しました。たとえば、積を計算している場合は、分割関数を並列に単純に適用し、最後に結合することができます。削減のプロセスでは、常にタイプがストリームになります。収集は、結果を変更可能なコンテナに収集する場合、つまり結果がストリームとは異なるタイプである場合に使用されます。これには、コンテナーの単一のインスタンスを各分割ストリームに使用できるという利点がありますが、コンテナーを最後に組み合わせる必要があるという欠点があります。
ボリスザスパイダー

1
製品の例の@ jimhooker2002 int不変なので、収集操作を簡単に使用できません。あなたは、AtomicIntegerまたはいくつかのカスタムを使用するような汚いハックをすることができましたが、IntWrapperなぜあなたはそうしますか?折りたたみ操作は、単に収集操作とは異なります。
Boris the Spider 14年

17
reduceストリームの要素とは異なるタイプのオブジェクトを返すことができる別のメソッドもあります。
damluar 2014年

1
uがreduceの代わりにcollectを使用するもう1つのケースは、reduce操作が要素をコレクションに追加する場合であり、アキュムレータ関数が要素を処理するたびに、要素を含む新しいコレクションを作成しますが、これは非効率的です。
ラグー

40

その理由は単に次のとおりです。

  • collect() 変更可能な結果オブジェクトでのみ機能します。
  • reduce()されて動作するように設計して不変の結果オブジェクト。

reduce()不変」の例

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(Integer.valueOf(6), sum);
}

collect()可変」の例

たとえば、collect()それを使用して合計を手動で計算したい場合は、たとえばfrom でBigDecimalしか機能しません。見る:MutableIntorg.apache.commons.lang.mutable

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new, 
    (MutableInt container, Employee employee) -> 
      container.add(employee.getSalary().intValue())
    , 
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

これが機能するの container.add(employee.getSalary().intValue());は、アキュムレータが結果とともに新しいオブジェクトを返すのではなくcontainer、type のmutable の状態を変更するためMutableIntです。

BigDecimal代わりにを使用したい場合containerは、collect()メソッドが不変でcontainer.add(employee.getSalary());あるcontainerため変更できないので、メソッドを使用できませんBigDecimal。(これとは別に、空のコンストラクタがないBigDecimal::newため機能しませんBigDecimal


2
Integerコンストラクター(new Integer(6))を使用していることに注意してください。これは、以降のJavaバージョンでは非推奨です。
MC皇帝

1
良いキャッチ@MCEmperor!変更しましたInteger.valueOf(6)
Sandro

@サンドロ-私は混乱しています。collect()が変更可能なオブジェクトでのみ機能すると言うのはなぜですか?文字列を連結するために使用しました。文字列allNames = employees.stream().map(Employee :: getNameString).collect(Collectors.joining( "、")).toString();
MasterJoe2

1
@ MasterJoe2それは簡単です。要するに、実装StringBuilderは変更可能なwhichをまだ使用しています。参照:hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/...
サンドロ・

30

通常の削減は、int、doubleなどの2つの不変値を組み合わせて新しい値を生成することを意味します。それは不変の削減です。対照的に、collectメソッドは、コンテナーを変更して、生成するはずの結果を蓄積するように設計されています。

問題を説明するために、次のCollectors.toList()ような単純な削減を使用して達成したいとします。

List<Integer> numbers = stream.reduce(
        new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
            l.add(e);
            return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
            l1.addAll(l2);
            return l1;
        });

これはと同等ですCollectors.toList()。ただし、この場合はを変更しList<Integer>ます。ArrayListはスレッドセーフではないことがわかっているため、反復中に値を追加/削除しても安全ではないためArrayIndexOutOfBoundsException、リストまたはコンバイナを更新すると、同時例外または任意の種類の例外(特に並列実行時)が発生します。リストに整数を累積(追加)して変更するため、リストをマージしようとします。このスレッドセーフにする場合は、毎回新しいリストを渡す必要があります。これにより、パフォーマンスが低下します。

対照的に、Collectors.toList()作品は同様の方法で動作します。ただし、値をリストに累積すると、スレッドの安全性が保証されます。メソッドドキュメントcollectから:

コレクターを使用して、このストリームの要素に対して変更可能な縮小操作を実行します。ストリームが並列で、コレクターが並行であり、ストリームが順序付けされていないか、コレクターが順序付けられていない場合、並行削減が実行されます。並列で実行すると、複数の中間結果がインスタンス化され、データが入力され、マージされて、可変データ構造の分離が維持されます。 したがって、スレッドセーフではないデータ構造(ArrayListなど)と並列に実行された場合でも、並列縮小のために追加の同期を行う必要はありません。

だからあなたの質問に答えるには:

いつcollect()vs を使用しreduce()ますか?

次のような不変の価値を持っている場合はintsdoublesStringsその後、通常の減少はうまく動作します。ただし、reduce値をList(可変データ構造)にする必要がある場合は、collectメソッドで可変リダクションを使用する必要があります。


コードスニペットでは、問題はID(この場合はArrayListの単一のインスタンス)を取得し、それが「不変」であると想定してx、スレッドを開始し、それぞれを「IDに追加」してから組み合わせることが問題だと思います。良い例え。
rogerdpack 2018年

なぜ同時変更例外が発生するのか、ストリームの呼び出しはシリアルストリームを再実行するだけであり、これはシングルスレッドによって処理され、結合関数がまったく呼び出されないことを意味します。
amarnathは、18

public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }私は試してみましたが、CCm例外が発生しませんでした
アマーナスは、

@amarnathharish問題を並列で実行しようとし、複数のスレッドが同じリストにアクセスしようとすると発生します
george

11

ストリームをa <-b <-c <-dとする

削減では、

((a#b)#c)#d

ここで、#は、実行したい興味深い操作です。

コレクションでは、

あなたのコレクターは、ある種の収集構造Kを持ちます。

Kはaを消費します。Kはbを消費します。Kはcを消費します。Kはdを消費します。

最後に、Kに最終結果を尋ねます。

Kはそれをあなたに与えます。


2

実行時の潜在的なメモリフットプリントは大きく異なります。一方でcollect()収集し、プットすべてのデータ収集に、reduce()明示的にストリームを介してそれを作ったデータを削減する方法を指定するように求められます。

たとえば、ファイルからデータを読み取って処理し、データベースに格納する場合、次のようなJavaストリームコードが生成される可能性があります。

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

この場合は、collect()Javaにデータをストリーミングさせ、結果をデータベースに保存させるために使用します。せずにcollect()データ読んだことがないと保存されることはありません。

このコードjava.lang.OutOfMemoryError: Java heap spaceは、ファイルサイズが十分に大きいか、ヒープサイズが十分に小さい場合に、実行時エラーを生成します。明らかな理由は、ストリームを介して作成されたすべてのデータ(実際には既にデータベースに格納されている)を結果のコレクションにスタックしようとし、これによりヒープが爆破されるためです。

しかし、あなたが交換した場合collect()reduce()-後者はを通してそれを作ったすべてのデータを削減し、廃棄するとして、それはもう問題になることはありません。

提示された例では、次のものに置き換えcollect()ますreduce

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

resultJavaは純粋なFP(関数型プログラミング)言語ではないため、計算に依存するように注意する必要もありません。副作用の可能性があるため、ストリームの下部で使用されていないデータを最適化できません。 。


3
db saveの結果を気にしない場合は、forEach ...を使用する必要があります。reduceを使用する必要はありません。これが説明を目的としたものでない限り。
DaveEdelstein 16

2

これがコード例です

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

System.out.println(sum);

実行結果は次のとおりです。

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

Reduce関数は2つのパラメーターを処理します。最初のパラメーターはストリームの以前の戻り値、2番目のパラメーターはストリームの現在の計算値、最初の値と現在の値を次の計算の最初の値として合計します。


0

ドキュメントによると

reduction()コレクターは、groupingByまたはpartitioningByのダウンストリームであるマルチレベルのリダクションで使用する場合に最も役立ちます。ストリームで単純な削減を実行するには、代わりにStream.reduce(BinaryOperator)を使用します。

したがって、基本的にはreducing()、収集内で強制された場合にのみ使用します。次に別の例を示します。

 For example, given a stream of Person, to calculate the longest last name 
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

このチュートリアルによると reduceは効率が低下することがあります

reduce操作は常に新しい値を返します。ただし、アキュムレータ関数は、ストリームの要素を処理するたびに新しい値も返します。ストリームの要素を、コレクションなどのより複雑なオブジェクトに削減するとします。これは、アプリケーションのパフォーマンスを低下させる可能性があります。削減操作で要素をコレクションに追加する必要がある場合、アキュムレータ関数が要素を処理するたびに、要素を含む新しいコレクションが作成されますが、これは非効率的です。代わりに、既存のコレクションを更新する方が効率的です。これは、次のセクションで説明するStream.collectメソッドで実行できます...

そのため、IDは削減シナリオで「再利用」されるため.reduce、可能であれば少し効率的です。

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