1. reduce メソッド: 汎用的な畳み込み
プログラミングでは、コレクションを1つの最終的な値に「畳み込む」必要がよくあります。たとえば合計を求める、積を計算する、文字列を連結する、集約メトリクスを算出する、新しい構造に要素を集める、などです。かつてはループと蓄積用の変数で手作業で書いていましたが、今では Stream API によって、汎用メソッドの reduce と collect を使ったエレガントな方法で、簡潔かつ宣言的に記述できます。
- reduce は、ストリームを合計・積・連結などの1つの最終値に畳み込みます。
- collect は、ストリームをコレクション、文字列、マップ、または任意の構造へ変換します。
では、順を追って見ていきましょう。
reduce は何のためにあるのか?
reduce は終端メソッドで、要素を関数型のアキュムレータ(蓄積関数)で「畳み込んで」単一の値にします。概念的には、コレクションを走査しながら段階的に結果を更新していくイメージです。
reduce のメソッドシグネチャ
Stream API には、主に3つの reduce() があります:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- accumulator — 現在の蓄積値と次の要素を受け取り、新しい結果を返す関数。
- identity — アキュムレータの初期値(例:合計なら 0、積なら 1)。
- combiner — 並列ストリームで中間結果を結合するために使われます。
reduce の使用例
例1: 数の合計
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// identity なしの reduce: 結果は Optional
Optional<Integer> sum1 = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.orElse(0)); // 15
// identity ありの reduce: 常に結果がある
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 15
例2: すべての数の積
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 120
例3: 文字列の連結
List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API
例4: 最大要素の検索
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(System.out::println); // 5
例5: すべての文字列の長さの合計
List<String> texts = List.of("cat", "dog", "elephant");
int totalLength = texts.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(totalLength); // 14
reduce の動作原理
reduce のロジックは次のループと同等です:
T result = identity;
for (T element : collection) {
result = accumulator.apply(result, element);
}
return result;
identity が指定されていない場合、開始値はストリームの最初の要素になり、メソッドは Optional を返します(ストリームが空なら空のまま)。
2. collect メソッド: 汎用的な変換
collect は、ストリームをコレクション、文字列、マップ、または任意の構造へ変換する終端メソッドです。このために「コレクタ」(Collector) を使います。多くの場合はクラス Collectors に用意された既製のコレクタを利用します。
代表的なコレクタ
- Collectors.toList() — 要素を List に集めます。
- Collectors.toSet() — 要素を Set に集めます。
- Collectors.toMap() — 要素を Map に集めます。
- Collectors.joining() — 文字列を1つに連結します。
- Collectors.groupingBy() — 要素を特性でグループ化します。
- Collectors.counting() — 要素数を数えます。
- Collectors.summarizingInt() — 数値の統計(合計、平均、最小/最大)を集計します。
collect の使用例
例1: リストに集める
List<String> names = List.of("Anya", "Boris", "Vasya", "Anya");
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames); // [Anya, Boris, Vasya]
例2: 集合 (Set) に集める
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println(nameSet); // [Anya, Boris, Vasya] (順序は保証されません)
例3: 文字列に集約する
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv); // Anya, Boris, Vasya, Anya
例4: Map に集める
次のクラスがあるとします:
public class Employee {
private String name;
private String department;
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getName() { return name; }
public String getDepartment() { return department; }
}
「名前 → 部署」のマップを作ります:
List<Employee> employees = List.of(
new Employee("Anya", "IT"),
new Employee("Boris", "HR"),
new Employee("Vasya", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // 重複する名前の処理
));
System.out.println(nameToDept); // {Anya=IT, Boris=HR, Vasya=IT}
例5: 一意な要素を Set に集める
Set<String> unique = names.stream()
.collect(Collectors.toSet());
System.out.println(unique);
例6: 数値の統計を集める
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax()); // 5
System.out.println(stats.getMin()); // 1
3. 比較: reduce と collect をいつ使うか
reduce は、二項演算で単一の最終値を得たいとき(合計、積、最大、連結など)に使います。
collect は、要素をコレクション/マップ/文字列に集めたり、Collector を使ってより複雑な集約を行いたいときに使います。これらのタスクでは、通常 collect のほうが強力で効率的です。
表: reduce と collect の比較
| タスク | 使うもの | 例 |
|---|---|---|
| 数の合計 | |
|
| 積 | |
|
| List に集める | |
|
| Map に集める | |
|
| グルーピング | |
|
| 文字列の連結 | reduce / collect | reduce("", String::concat) または Collectors.joining() |
4. 実践課題
課題1: リスト中のすべての文字列の長さの合計を求める
List<String> words = List.of("cat", "dog", "elephant");
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // または reduce を使う: .reduce(0, Integer::sum)
System.out.println(totalLength); // 14
課題2: 一意な要素を Set に集める
List<String> fruits = List.of("りんご", "洋梨", "りんご", "オレンジ");
Set<String> uniqueFruits = fruits.stream()
.collect(Collectors.toSet());
System.out.println(uniqueFruits); // [りんご, 洋梨, オレンジ]
課題3: オブジェクトのリストから Map を構築する
List<Employee> employees = List.of(
new Employee("Anya", "IT"),
new Employee("Boris", "HR"),
new Employee("Vasya", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // 名前が重複する場合
));
System.out.println(nameToDept);
課題4: すべての名前をカンマ区切りで連結
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(allNames); // Anya, Boris, Vasya
5. 実装上の特徴と注意点
Optional と reduce
reduce を identity なしで使うと、結果は Optional になります。これは安全で、ストリームが空なら結果も空です。ifPresent(...)、orElse(...)、orElseThrow(...) などで適切に扱うのを忘れないでください。
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
独自コレクタ: どうしても必要なときだけ
標準のコレクタで足りない場合は、独自の Collector を実装できます。ただし、99% のケースでは Collectors の既製コレクタで十分です。
コレクタと並列ストリーム
Collectors の既製コレクタは、parallelStream() でも正しく動作するよう設計されています。並列ストリーム上の forEach 内で、共有の可変コレクションに手動で要素を追加するのは避けましょう。データレースを引き起こします。
6. reduce と collect のよくある誤り
誤り1: reduce の後の Optional を確認しない。 ストリームが空の場合、reduce(identity なし)は空の Optional を返します。get() を呼ぶと NoSuchElementException になります。ifPresent、orElse、orElseThrow を使いましょう。
誤り2: コレクションを reduce で集めようとする。 可能ではありますが、この用途には collect のほうが適切で高速です:
// 非効率!
List<String> list = stream.reduce(
new ArrayList<>(),
(acc, elem) -> { acc.add(elem); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// こちらの方が良い:
List<String> list2 = stream.collect(Collectors.toList());
誤り3: toMap でキーの重複を処理しない。 キーが重複すると例外が発生します。衝突を解決するために toMap の第3引数を指定しましょう。
誤り4: 並列ストリームで同期なしに可変コレクションを使う。 collect では標準のコレクタを使いましょう。並列でも正しく動作します。parallelStream() 上の forEach で list.add() を行うのは避けてください。
誤り5: 複雑なタスクで reduce と collect を取り違える。 reduce は単純な集約(合計、最大など)向けです。collect はコレクションへの収集、グルーピング、Map の構築や複合的な集約に適しています。
GO TO FULL VERSION