7.42:ラムダとストリーム
要約
ラムダとストリームはコードを簡潔で宣言的にし、並列化の扉も開く強力な道具。だが可読性・副作用・性能に注意して、適材適所で使うべき。
見本
短くて意図が分かりやすい — これがラムダ&ストリームの強み。
// 従来(命令型)
List<String> names = new ArrayList<>();
for (Person p : people) {
if (p.getAge() >= 20) names.add(p.getName().toUpperCase());
}
// ストリーム+ラムダ(宣言型)
List<String> names = people.stream()
.filter(p -> p.getAge() >= 20)
.map(p -> p.getName().toUpperCase())
.collect(Collectors.toList());
良いパターン(推奨)
-
標準の関数型インタフェースを使う:
Function,Predicate,Consumer,Supplier
Function<Employee, String> getName = Employee::name; // Function<T,R>
Predicate<Employee> isAdult = e -> e.age() >= 20; // Predicate<T>
Consumer<Employee> printer = e -> System.out.println(e.name()); // Consumer<T>
Supplier<List<Employee>> supplier = ArrayList::new; // Supplier<T>
List<Employee> list = List.of(
new Employee("Alice", 30, Department.ENG),
new Employee("Bob", 19, Department.SALES),
new Employee("Carol", 25, Department.ENG)
);
list.stream()
.filter(isAdult)
.map(getName)
.forEach(printer);
-
メソッド参照を活用:
map(Person::getName)は読みやすい
// map(e -> e.name()) の代わりに
List<String> names = list.stream()
.map(Employee::getName) // メソッド参照
.collect(Collectors.toList());
-
プリミティブ専用ストリームを使う(
IntStream/LongStream)でボクシングを避ける
// 年齢の合計(ボクシングを避ける)
int totalAge = list.stream()
.mapToInt(Employee::age) // IntStream
.sum();
// 平均
OptionalDouble avg = list.stream()
.mapToInt(Employee::age)
.average();
-
Collectors を使った集約:
Collectors.toList(),groupingBy,partitioningBy,toMap
// toList
List<Employee> engs = list.stream()
.filter(e -> e.department() == Department.ENG)
.collect(Collectors.toList());
// toMap (キー重複時の解決)
Map<String, Employee> byName = list.stream()
.collect(Collectors.toMap(Employee::name, Function.identity(),
(existing, replacement) -> existing)); // 衝突は先のものを採る
// groupingBy(次のセクションで詳述)
Map<Department, List<Employee>> byDept = list.stream()
.collect(Collectors.groupingBy(Employee::department));
// partitioningBy(Predicate による2分割)
Map<Boolean, List<Employee>> adultsPartition = list.stream()
.collect(Collectors.partitioningBy(e -> e.age() >= 20));
-
短絡(short-circuit)オペレーションを利用:
anyMatch,allMatch,findFirst,limit
boolean anyAdult = list.stream().anyMatch(e -> e.age() >= 20); // 早期終了可
boolean allAdults = list.stream().allMatch(e -> e.age() >= 20);
Optional<Employee> firstEng = list.stream().filter(e -> e.department()==Department.ENG).findFirst();
List<Employee> limited = list.stream().limit(2).collect(Collectors.toList());
-
副作用を避ける:
map/filter内で状態を変更しない。最終段のforEachで副作用するなら限定的に
Map<Department, List<Employee>> byDepartment =
list.stream()
.collect(Collectors.groupingBy(Employee::department));
// さらに各部門の年齢平均を求める例
Map<Department, Double> avgAgeByDept =
list.stream()
.collect(Collectors.groupingBy(Employee::department,
Collectors.averagingInt(Employee::age)));
// 出力例
byDepartment.forEach((dept, emps) ->
System.out.println(dept + ": " + emps.stream().map(Employee::name).collect(Collectors.joining(", ")))
);
avgAgeByDept.forEach((dept, avg) ->
System.out.println(dept + " average age: " + avg)
);
悪いパターン
-
ストリームで副作用
スレッド安全でない可変変数への書き込みなど
List<Integer> result = new ArrayList<>();
items.stream().forEach(i -> result.add(i)); // NG:副作用、スレッド安全性がない
-
並列ストリームで可変状態を共有(
parallelStream()とArrayListを同時に操作するなど)
Map<String, Long> counts = new HashMap<>();
items.parallelStream().forEach(i -> counts.merge(i.type(), 1L, Long::sum)); // race!
-
ストリームを2回使おうとする
ストリームは単回使用
Stream<String> s = items.stream().filter(...);
s.forEach(...);
s.forEach(...); // IllegalStateException
-
チェック例外をラムダ内で直接投げる
コンパイルの扱いが面倒
items.stream().map(s -> {
return Files.readString(Paths.get(s)); // throws IOException -> compile error
});
-
無意味に複雑なパイプライン
1 行が長すぎて読みづらい
List<String> r = items.stream()
.filter(i -> i.isActive() && i.getDate().isAfter(someDate) && complexCheck(i))
.map(i -> i.getSub().flatMap(x -> x.map(y -> compute(y)).orElse("X")))
.flatMap(s -> Arrays.stream(s.split(",")))
.distinct().sorted()
.collect(Collectors.toList());
まとめ
- 副作用は避け、結果は
collectで受け取る - 並列ストリームで共有ミュータブル状態を書き換えない
- ストリームは一度しか使えない ➡ 必要なら
Supplier<Stream>かcollectで保存 - ラムダでチェック例外が出る処理はラップするか、従来ループで書く
- 長いチェーンはメソッド抽出で読みやすく
-
parallel()は測定してから使う