Java | Java 詳細・モダン文法:Stream API 深掘り – reduce

Java Java
スポンサーリンク

reduce を一言でいうと

Stream#reduce は、
「ストリームに流れてくる複数の要素を、“1 つの値”に畳み込むための終端操作」です。

全部を 1 つに“まとめる”イメージです。
合計を出す、最大値を求める、文字列を連結する、オブジェクトに集約する――こういう処理の“本質”は全部 reduce です。


まずは一番シンプルな reduce から

Optional<T> reduce(BinaryOperator<T>) の形

一番基本のシグネチャはこれです。

Optional<T> reduce(BinaryOperator<T> accumulator)
Java

BinaryOperator<T> は、
「T と T を受け取って、T を返す関数」= (T, T) -> T です。

つまり、

「今までの結果」と「次の要素」を 1 つにまとめる関数

を渡すことで、ストリーム全体を 1 つの値に畳み込んでいきます。

例:整数の合計を reduce で書く

import java.util.List;
import java.util.Optional;

public class ReduceSumBasic {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5);

        Optional<Integer> sum =
                nums.stream()
                    .reduce((a, b) -> a + b);

        System.out.println(sum); // Optional[15]
    }
}
Java

ここでの reduce((a, b) -> a + b) は、
「今までの合計 a と、次の要素 b を足して、新しい合計にする」
という意味です。

ストリームが空の場合は「畳み込む元がない」ので、結果は Optional.empty() になります。


identity 付き reduce で Optional を避ける

T reduce(T identity, BinaryOperator<T> accumulator)

「空のときでも、必ず何かしらの値を返したい」場合は、
初期値(identity)を渡すオーバーロードを使います。

T reduce(T identity, BinaryOperator<T> accumulator)
Java

identity は「畳み込みの初期値」です。

例:合計を identity 付きで書く

import java.util.List;

public class ReduceSumIdentity {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5);

        int sum =
                nums.stream()
                    .reduce(0, (a, b) -> a + b);

        System.out.println(sum); // 15
    }
}
Java

ここでは、identity = 0accumulator = (a, b) -> a + b です。

ストリームが空でも、
「何も足されない 0」として結果が返ってきます。

identity の意味をちゃんと理解する

identity には、
「畳み込みにおける単位元(neutral element)」
を渡すのが基本です。

足し算なら 0
掛け算なら 1
文字列連結なら “”

といった具合です。

これを守らないと、結果がズレたり、並列処理時に破綻したりします。


reduce の 3 引数版(少しだけ応用)

U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

3 引数版は、主に並列ストリームや「型を変えながら畳み込む」場面で使います。

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner)
Java

ざっくり言うと、

  • identity:初期値(結果の型 U)
  • accumulator:(U, T) -> U(結果と要素をどう畳み込むか)
  • combiner:(U, U) -> U(部分結果同士をどうマージするか)

です。

初心者のうちは、
「型を変えたいときは collect を使う」
「3 引数 reduce は“そういうのもある”くらいで OK」
と覚えておくといいです。


よくあるパターンを reduce で書いてみる

合計・最大値・最小値

合計:

int sum = nums.stream()
              .reduce(0, (a, b) -> a + b);
Java

最大値:

int max = nums.stream()
              .reduce(Integer.MIN_VALUE, (a, b) -> Math.max(a, b));
Java

最小値:

int min = nums.stream()
              .reduce(Integer.MAX_VALUE, (a, b) -> Math.min(a, b));
Java

もちろん、mapToInt().sum()max() / min() などの専用メソッドもありますが、
「本質的には reduce で書ける」という感覚を持っておくと理解が深まります。

文字列の連結

import java.util.List;

public class ReduceJoin {
    public static void main(String[] args) {
        List<String> words = List.of("Java", "Stream", "reduce");

        String joined =
                words.stream()
                     .reduce("", (a, b) -> a + " " + b)
                     .trim();

        System.out.println(joined); // "Java Stream reduce"
    }
}
Java

ここでは、
「今までの文字列 a に、スペースと次の単語 b をくっつける」
という畳み込みをしています。


reduce を設計するときに絶対に意識したいこと

演算は「結合的」であるべき

特に並列ストリームで reduce を使うとき、
畳み込みの演算は「結合的(associative)」である必要があります。

結合的とは、

f(f(a, b), c) == f(a, f(b, c))
Java

が成り立つことです。

足し算、掛け算、min / max などは結合的です。
一方、「順番に依存する処理」や「副作用を伴う処理」は結合的ではないことが多いです。

結合的でない演算を reduce に渡すと、
並列処理時に結果が変わったり、バグの元になります。

副作用を reduce に入れない

reduce の中で、
ログを書いたり、外部のリストに add したり、カウンタをインクリメントしたり――
といった「副作用」を書き始めると、一気にコードが壊れやすくなります。

副作用は forEachpeek に任せて、
reduce は「純粋に値を畳み込む」ことだけに集中させるのが、きれいな設計です。


reduce と collect の違いをざっくり整理する

「1 つの値」か、「コンテナに集める」か

reducecollect は、どちらも「ストリームを 1 つのものにまとめる」終端操作です。

ざっくり分けると、

  • 単純な値(数値、1 つのオブジェクトなど)に畳み込みたい → reduce
  • List / Set / Map などのコレクションに集めたい → collect

という使い分けになります。

例えば、「合計」「最大値」「フラグの AND / OR」などは reduce が自然です。
一方、「リストに集める」「グルーピングする」などは collect の方が圧倒的に書きやすいです。


まとめ:reduce を自分の言葉で定義する

あなたの言葉で reduce をまとめるなら、こうなります。

reduce は、(今までの結果, 次の要素) -> 新しい結果 という関数を使って、
ストリーム全体を 1 つの値に畳み込む終端操作」

特に意識しておきたいのは、

「合計・最大値・連結」などの“まとめる処理”は、全部 reduce で書けること
identity 付きの reduce では、“単位元”を渡すこと(足し算なら 0、掛け算なら 1 など)
演算は結合的であるべきで、副作用は入れないこと
「値 1 つ」なら reduce、「コレクションに集める」なら collect というざっくりした使い分け

あたりです。

タイトルとURLをコピーしました