ここでは、Stream APIの「落とし穴(注意点)」と「安全に使うための設計・ベストプラクティス」を、実務コード例とともに解説します。
Stream API の落とし穴と安全設計
落とし穴①:副作用(外部変数の変更)
❌ 悪い例
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> result = new ArrayList<>();
// forEachの中で外部リストを変更(副作用あり)
names.stream().forEach(name -> {
if (name.startsWith("A")) {
result.add(name); // ← 外部リストへの書き込み
}
});
Java- 問題点:
forEachの中で外部のresultを変更している。
並列ストリーム (parallelStream) にすると、スレッド競合が起こり得る。 - 症状:意図しない重複、順序の乱れ、
ConcurrentModificationExceptionなど。
✅ 安全な書き方
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.toList(); // 副作用なし、安全
Javaポイント
- Stream内で「外部の変数を変更しない」
filter/map/collectで「純粋な関数型処理」に徹する
落とし穴②:例外処理が書きづらい
❌ 悪い例
List<String> files = List.of("a.txt", "b.txt", "c.txt");
files.stream()
.map(file -> Files.readString(Path.of(file))) // IOException が投げられる!
.forEach(System.out::println);
Javamap内でチェック例外(IOException)を投げるとコンパイルエラー。
Stream APIはチェック例外を直接扱えない。
✅ 安全な書き方(try-catchをラップ)
List<String> files = List.of("a.txt", "b.txt", "c.txt");
files.stream()
.map(file -> {
try {
return Files.readString(Path.of(file));
} catch (IOException e) {
// ログ出力などの安全対応
System.err.println("読み込み失敗: " + file + " → " + e.getMessage());
return ""; // フォールバック値
}
})
.filter(content -> !content.isEmpty())
.forEach(System.out::println);
Javaポイント
map内で安全にtry-catchする- 例外時のフォールバック値やロギング方針を決めておく
落とし穴③:null の扱い
❌ 悪い例
List<String> list = List.of("A", null, "B");
list.stream()
.filter(s -> s.startsWith("A")) // NullPointerException!
.forEach(System.out::println);
Java✅ 安全な書き方
List<String> list = List.of("A", null, "B");
list.stream()
.filter(Objects::nonNull)
.filter(s -> s.startsWith("A"))
.forEach(System.out::println);
Javaポイント
Objects::nonNullでnull除外は定番- nullを混ぜない設計が最も安全(入力時チェック)
落とし穴④:ストリームの再利用
❌ 悪い例
Stream<String> stream = List.of("A", "B", "C").stream();
stream.forEach(System.out::println);
stream.filter(s -> s.equals("A")).count(); // IllegalStateException!
Java✅ 安全な書き方
List<String> list = List.of("A", "B", "C");
list.stream().forEach(System.out::println);
long count = list.stream()
.filter(s -> s.equals("A"))
.count();
Javaポイント
- Streamは1回限り使い捨て
- 再利用せず、都度
list.stream()を呼び直す
落とし穴⑤:parallelStream の誤用
❌ 悪い例
List<Integer> nums = IntStream.range(1, 10000).boxed().toList();
int sum = nums.parallelStream()
.mapToInt(i -> {
System.out.println(Thread.currentThread().getName()); // デバッグ出力混在
return i;
})
.sum();
Java- 並列化で出力がぐちゃぐちゃに混在
- I/Oやログを含む処理にparallelStreamを使うと非効率・不安定
✅ 安全な書き方
List<Integer> nums = IntStream.range(1, 10000).boxed().toList();
int sum = nums.parallelStream()
.mapToInt(Integer::intValue)
.sum(); // 純粋な計算ならOK
Javaポイント
parallelStream()はCPU計算専用- I/O処理・DBアクセスには不向き(並列数やスレッド安全性の問題)
実務向け安全設計まとめ
| 分類 | NG例 | 安全な書き方 |
|---|---|---|
| 副作用 | 外部リストにadd | collect()で新しいリスト作成 |
| 例外処理 | 直接throw | try-catchで安全にラップ |
| null処理 | そのまま使用 | Objects::nonNullで除外 |
| 再利用 | 同じstreamを再利用 | 都度 .stream() |
| 並列化 | parallel + I/O | 計算専用に限定 |
