Java | Stream API 安全設計ベストプラクティス(例外・ログ対応)

Java Java
スポンサーリンク

ここでは、実務で使う 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());
Java
  • map() で変換 → 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());
Java

4️⃣ 並列処理(parallelStream)の注意点

NG例

List<String> results = new ArrayList<>();
list.parallelStream().forEach(s -> results.add(s.toUpperCase()));
Java
  • ArrayList はスレッドセーフではない → データ競合

OK例①:Collecting と組み合わせる

List<String> results = list.parallelStream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
Java
  • collect() は並列でも安全

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());
Java
  • Optional.ofNullable() で null を回避
  • filter(Objects::nonNull) で安全に除外

6️⃣ 実務でのチェックリスト

チェック項目推奨アクション
外部変数への副作用map/collect で完結させる
例外処理ラムダ内で try/catch or ユーティリティ化
ロギングSLF4J で統一し、必要に応じて WARN/ERROR
parallelStreammutable 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除去・ログ統一を徹底することで安全に運用可能
Java
スポンサーリンク
シェアする
@lifehackerをフォローする
スポンサーリンク
タイトルとURLをコピーしました