ここでは、実務で使う Stream API の安全設計ベストプラクティスをまとめます。
特に、例外処理・ログ出力・副作用回避・大規模データ対応にフォーカスしています。
1️⃣ 基本ルール:副作用は極力避ける
NG例(副作用あり)
List<String> results = new ArrayList<>();
list.stream().forEach(s -> results.add(s.toUpperCase()));
Java- 問題点:
- Stream は基本的に関数型スタイル。副作用(外部変数の変更)は並列処理で危険
- 大規模データや
parallelStream()でバグになる可能性
OK例(副作用回避)
List<String> results = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Javamap()で変換 →collect()で結果を取得- 純粋関数型スタイルで安全
2️⃣ 例外処理の組み込みパターン
Stream 内で例外が発生すると 処理が途中で止まる ことがあります。
✅ 方法① try/catch をラムダ内で処理
list.stream()
.map(s -> {
try {
return process(s); // 例:API呼び出し
} catch (Exception e) {
System.err.println("[ERROR] 処理失敗: " + s + " " + e.getMessage());
return null;
}
})
.filter(Objects::nonNull) // null を除外
.collect(Collectors.toList());
Java- ポイント:
- 例外をキャッチしてログ出力
- null を除外して安全に Stream を完結させる
✅ 方法② ユーティリティ関数化
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> func) {
return t -> {
try {
return func.apply(t);
} catch (Exception e) {
System.err.println("[ERROR] " + t + " 処理失敗: " + e.getMessage());
return null;
}
};
}
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
// 使用例
List<String> processed = list.stream()
.map(wrap(MyClass::process))
.filter(Objects::nonNull)
.collect(Collectors.toList());
Java- 長い Stream 内の処理でも 例外処理を共通化
- ログ出力ルールを統一可能
3️⃣ ロギング対応のベストプラクティス
- 標準出力(System.out/err)は開発用のみ)
- 実務では SLF4J / Logback / Log4j2 を使用
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
list.stream()
.map(s -> {
try {
return process(s);
} catch (Exception e) {
logger.error("処理失敗: {}", s, e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
Java4️⃣ 並列処理(parallelStream)の注意点
NG例
List<String> results = new ArrayList<>();
list.parallelStream().forEach(s -> results.add(s.toUpperCase()));
JavaArrayListはスレッドセーフではない → データ競合
OK例①:Collecting と組み合わせる
List<String> results = list.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Javacollect()は並列でも安全
OK例②:スレッドセーフなコレクションを使用
List<String> results = Collections.synchronizedList(new ArrayList<>());
list.parallelStream().forEach(s -> results.add(s.toUpperCase()));
Java- 小規模の場合のみ推奨。大規模は
collect()がベター
5️⃣ null 安全設計
Stream 内で null が混在すると NullPointerException になることがあります。
対策例
List<String> safeList = Optional.ofNullable(list)
.orElse(Collections.emptyList())
.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.collect(Collectors.toList());
JavaOptional.ofNullable()で null を回避filter(Objects::nonNull)で安全に除外
6️⃣ 実務でのチェックリスト
| チェック項目 | 推奨アクション |
|---|---|
| 外部変数への副作用 | map/collect で完結させる |
| 例外処理 | ラムダ内で try/catch or ユーティリティ化 |
| ロギング | SLF4J で統一し、必要に応じて WARN/ERROR |
| parallelStream | mutable collection への書き込みは避ける |
| null 値 | Optional + filter(Objects::nonNull) で安全化 |
| 大規模データ | filter/map/collect のみでパイプライン化 |
7️⃣ サンプル:安全設計テンプレート
List<String> processed = Optional.ofNullable(list)
.orElse(Collections.emptyList())
.parallelStream() // 並列処理
.map(s -> {
try {
return process(s); // 外部APIや計算
} catch (Exception e) {
logger.error("処理失敗: {}", s, e);
return null;
}
})
.filter(Objects::nonNull) // null除去
.map(String::toUpperCase) // 安全な変換
.collect(Collectors.toList());
Java- ✅ 並列でも安全
- ✅ 例外があっても処理が止まらない
- ✅ ログ出力・副作用なし
- ✅ null 安全
💡 まとめ
- Stream API は強力だが、副作用・例外・並列処理・nullに注意
- 実務では「ラムダ内例外処理 + ログ + collect()で完結」が基本
parallelStream()は安全設計を徹底しないとバグの温床- 副作用ゼロ・例外キャッチ・null除去・ログ統一を徹底することで安全に運用可能

