Stream.reduce()へのガイド

1. 概要

link:/java-8-streams-introduction[Stream API]は、中間関数、リダクション関数、およびターミナル関数の豊富なレパートリーを提供し、並列化もサポートしています。
より具体的には、*縮小ストリーム操作では、シーケンス内の要素に結合操作を繰り返し適用することにより、要素のシーケンスから1つの結果を生成できます*。
このチュートリアルでは、*汎用https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html[_Stream.reduce()_]操作*を見て、それをいくつか見ていきます。具体的なユースケース。

2. キーコンセプト:アイデンティティ、アキュムレータ、コンバイナ

_Stream.reduce()_操作の使用をさらに詳しく見る前に、操作の参加要素を個別のブロックに分解しましょう。 そうすれば、それぞれが果たす役割をより簡単に理解できます。
  • Identity –削減の初期値である要素
    操作とストリームが空の場合のデフォルトの結果

  • Accumulator – 2つのパラメーターを受け取る関数:部分的な結果
    リダクション操作とストリームの次の要素

  • Combiner –の部分的な結果を結合するために使用される関数
    リダクションが並列化されている場合、またはアキュムレーター引数のタイプとアキュムレーター実装のタイプの間に不一致がある場合のリダクション操作

*3. Stream.reduce() *の使用

アイデンティティ、アキュムレータ、およびコンバイナ要素の機能をよりよく理解するために、いくつかの基本的な例を見てみましょう。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers
  .stream()
  .reduce(0, (subtotal, element) -> subtotal + element);
assertThat(result).isEqualTo(21);
この場合、* _Integer_値0はIDです。*これは、リダクション操作の初期値、および_Integer_値のストリームが空のときのデフォルト結果も格納します。
同様に、*ラムダ式*:
subtotal, element -> subtotal + element
*は_Integer_値とストリーム内の次の要素の部分的な合計を取るため、アキュムレータです。
コードをさらに簡潔にするために、ラムダ式の代わりにメソッド参照を使用できます。
int result = numbers.stream().reduce(0, Integer::sum);
assertThat(result).isEqualTo(21);
もちろん、他のタイプの要素を保持するストリームで_reduce()_操作を使用できます。
たとえば、_String_要素の配列で_reduce()_を使用し、それらを1つの結果に結合できます。
List<String> letters = Arrays.asList("a", "b", "c", "d", "e");
String result = letters
  .stream()
  .reduce("", (partialString, element) -> partialString + element);
assertThat(result).isEqualTo("abcde");
同様に、メソッド参照を使用するバージョンに切り替えることができます。
String result = letters.stream().reduce("", String::concat);
assertThat(result).isEqualTo("abcde");
_letters_配列の大文字要素を結合するために_reduce()_操作を使用しましょう:
String result = letters
  .stream()
  .reduce("", (partialString, element) -> partialString.toUpperCase() + element.toUpperCase());
assertThat(result).isEqualTo("ABCDE");
さらに、並列化されたストリームで_reduce()_を使用できます(これについては後で詳しく説明します)。
List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32);
int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum);
ストリームが並行して実行されると、Javaランタイムはストリームを複数のサブストリームに分割します。 そのような場合、サブストリームの結果を単一のものに結合する関数を使用する必要があります。 *これはコンバイナーの役割です*上記のスニペットでは、_Integer

sum_メソッド参照です。

面白いことに、このコードはコンパイルされません。
List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));
int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());
この場合、_User_オブジェクトのストリームがあり、アキュムレーターの引数のタイプは_Integer_および_User._ですが、アキュムレーターの実装は_Integers_の合計であるため、コンパイラーは_user_のタイプを推測できませんパラメータ。
コンバイナーを使用してこの問題を修正できます。
int result = users.stream()
  .reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
assertThat(result).isEqualTo(65);
*簡単に言うと、順次ストリームを使用し、アキュムレータ引数の型とその実装の型が一致する場合、コンバイナを使用する必要はありません*。

4. 並列削減

前に学んだように、並列化されたストリームで_reduce()_を使用できます。
並列化されたストリームを使用する場合、_reduce()_またはストリームで実行される他の集約操作が次のことを確認する必要があります。
  • associative:結果はオペランドの順序に影響されません

  • non-interfering:操作はデータソースに影響しません

  • stateless_および_deterministic:操作に状態がなく、
    指定された入力に対して同じ出力を生成します

    予測できない結果を防ぐために、これらすべての条件を満たすべきです。
    予想どおり、_reduce()、_を含む並列化されたストリームで実行される操作は並列で実行されるため、マルチコアハードウェアアーキテクチャを利用しています。
    明らかな理由により、*並列化されたストリームは、対応する順次ストリームよりもはるかに高性能です*。 それでも、ストリームに適用される操作に費用がかからない場合、またはストリーム内の要素の数が少ない場合、それらは過剰になります。
    もちろん、並列化されたストリームは、大きなストリームを処理し、高価な集約操作を実行する必要がある場合に適切な方法です。
    単純なlink:/java-microbenchmark-harness[JMH](Java Microbenchmark Harness)ベンチマークテストを作成し、順次および並列化で_reduce()_操作を使用する場合のそれぞれの実行時間を比較しましょうストリーム:
@State(Scope.Thread)
private final List<User> userList = createUsers();

@Benchmark
public Integer executeReduceOnParallelizedStream() {
    return this.userList
      .parallelStream()
      .reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
}

@Benchmark
public Integer executeReduceOnSequentialStream() {
    return this.userList
      .stream()
      .reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
}
*上記のJMHベンチマークでは、実行平均時間を比較しています*。 単純に、多数の_User_オブジェクトを含む_List_を作成します。 次に、順次および並列化されたストリームで_reduce()_を呼び出し、後者が前者よりも高速に実行されることを確認します(操作あたりの秒数で)。
ベンチマーク結果は次のとおりです。
Benchmark                                                   Mode  Cnt  Score    Error  Units
JMHStreamReduceBenchMark.executeReduceOnParallelizedStream  avgt    5  0,007 ±  0,001   s/op
JMHStreamReduceBenchMark.executeReduceOnSequentialStream    avgt    5  0,010 ±  0,001   s/op

* 5. * スローおよび 削減中の例外処理

上記の例では、_reduce()_操作は例外をスローしません。 しかし、もちろんそうかもしれません。
たとえば、ストリームのすべての要素を指定された係数で除算してから合計する必要があるとします。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int divider = 2;
int result = numbers.stream().reduce(0, a / divider + b / divider);
_divider_変数がゼロでない限り、これは機能します。 ただし、ゼロの場合、_reduce()_はhttps://docs.oracle.com/javase/8/docs/api/java/lang/ArithmeticException.html[_ArithmeticException_]例外:ゼロによる除算をスローします。
link:/java-exceptions[try/ catch]ブロック:
public static int divideListElements(List<Integer> values, int divider) {
    return values.stream()
      .reduce(0, (a, b) -> {
          try {
              return a / divider + b / divider;
          } catch (ArithmeticException e) {
              LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
          }
          return 0;
      });
}
このアプローチは機能しますが、*ラムダ式を_try / catch_ブロックで汚染しました*。 以前のようなきれいなワンライナーはもうありません。
この問題を修正するには、https://refactoring.com/catalog/extractFunction.html [抽出関数のリファクタリング手法] **を使用し、_try / catch_ブロックを別のメソッドに抽出します**:
private static int divide(int value, int factor) {
    int result = 0;
    try {
        result = value / factor;
    } catch (ArithmeticException e) {
        LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
    }
    return result
}
これで、_divideListElements()_メソッドの実装が再びクリーンで合理化されました。
public static int divideListElements(List<Integer> values, int divider) {
    return values.stream().reduce(0, (a, b) -> divide(a, divider) + divide(b, divider));
}
_divideListElements()_が抽象_NumberUtils_クラスによって実装されるユーティリティメソッドであると仮定すると、_divideListElements()_メソッドの動作を確認する単体テストを作成できます。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
指定された_Integer_値の_List_に0が含まれる場合、_divideListElements()_メソッドもテストしましょう。
List<Integer> numbers = Arrays.asList(0, 1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
最後に、ディバイダーが0の場合のメソッド実装をテストしましょう。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 0)).isEqualTo(0);

6. 結論

このチュートリアルでは、_Stream.reduce()_操作の使用方法を学習しました。 さらに、シーケンシャルおよび並列化されたストリームで削減を実行する方法、および削減しながら例外を処理する方法を学びました*。
いつものように、このチュートリアルに示されているすべてのコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/java-streams-2[GitHub上]で入手できます。