ここでは 実務でよくある Stream API のアンチパターン集 を、初心者でも理解できるように「やってはいけない例 → 理由 → 改善例」のセットでまとめます。
1. 副作用を持つ forEach の乱用
❌ NG例
List<String> list = List.of("apple", "banana", "cherry");
List<String> result = new ArrayList<>();
list.stream().forEach(s -> result.add(s.toUpperCase()));
Java- 問題点
- Stream の利点である「宣言的・副作用なし」が失われる
- 並列処理(
parallelStream())にすると不具合が出やすい
- 改善例
List<String> result = list.stream()
.map(String::toUpperCase)
.toList();
Java✅ map → collect に置き換えることで副作用ゼロ、並列化も安全
2. Stream 内で例外を投げる
❌ NG例
List<String> list = List.of("1", "2", "a", "4");
list.stream()
.map(Integer::parseInt) // "a" があると NumberFormatException
.forEach(System.out::println);
Java- 問題点
- Stream は遅延実行で例外がどこで起きるか分かりにくい
- 全体処理が止まってしまう
- 改善例
List<Integer> nums = list.stream()
.map(s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return null;
}
})
.filter(Objects::nonNull)
.toList();
Java✅ 例外をハンドリングして安全に流せる
3. 重複した .stream() 呼び出し
❌ NG例
List<String> list = List.of("a", "b", "c");
long count = list.stream().count();
long nonEmpty = list.stream().filter(s -> !s.isEmpty()).count();
Java- 問題点
- 同じリストに対して複数回 Stream を作る → 冗長・性能悪化
- 改善例
long nonEmptyCount = list.stream()
.filter(s -> !s.isEmpty())
.count();
Java✅ 1つの Stream でまとめて処理
4. 不要な parallelStream() の多用
❌ NG例
List<Integer> list = List.of(1,2,3,4,5);
list.parallelStream()
.forEach(System.out::println);
Java- 問題点
- 小規模データだと逆に遅くなる
- 順序保証がない場合がある(println の順番が乱れる)
- 改善例
list.stream()
.forEach(System.out::println);
Java✅ 並列化は大量データでのみ使用、順序を意識する場合は注意
5. ネストした Stream の乱用
❌ NG例
List<List<String>> data = List.of(
List.of("A", "B"),
List.of("C", "D")
);
data.stream()
.map(inner -> inner.stream()
.map(String::toLowerCase)
.toList())
.toList();
Java- 問題点
map内でstream().toList()→ ネストされた Stream が残る- 可読性・保守性が低下
- 改善例
List<String> flat = data.stream()
.flatMap(List::stream)
.map(String::toLowerCase)
.toList();
Java✅ flatMap でフラット化してから処理するとスッキリ
6. 遅延実行の認識不足
❌ NG例
Stream<String> stream = list.stream()
.filter(s -> {
System.out.println("filter: " + s);
return s.length() > 3;
});
// println を期待しても出力されない
Java- 問題点
- Stream は中間操作(filter, mapなど)は遅延実行
- 末端操作(collect, forEach)が呼ばれるまで実行されない
- 改善例
list.stream()
.filter(s -> {
System.out.println("filter: " + s);
return s.length() > 3;
})
.toList();
Java✅ 末端操作を呼ぶことで意図通りに実行される
まとめ:実務で意識すべきルール
| アンチパターン | 改善のコツ |
|---|---|
| forEach で副作用 | map → collect に変える |
| Stream 内で例外 | try/catch で安全に処理 |
| 同じリストで複数 Stream | 1つにまとめる |
| parallelStream の乱用 | 大規模データのみ、順序注意 |
| ネストした Stream | flatMap を使ってフラット化 |
| 遅延実行を無視 | 必ず末端操作で Stream を確定 |
💡 実務の心得
- Stream は「宣言的で副作用なし」を意識
- 可読性・安全性を優先
- 小規模処理は無理に Stream にせず拡張 for でOK
