Java 逆引き集 | Stream の例外リカバリ戦略(fallback) — 途中失敗の処理

Java Java
スポンサーリンク

Stream の例外リカバリ戦略(fallback) — 途中失敗の処理

ストリーム処理の途中で I/O や変換が失敗しても、処理全体を止めずに「回復」させるのが fallback。目的は「止めるべき失敗」と「回復可能な失敗」を分けて、データロスと混乱を最小化することです。初心者向けに使い分けしやすい実戦パターンを、コードとテンプレートでまとめます。


基本方針(どれを選ぶ?)

  • デフォルト値で置き換える: 失敗要素を「安全な値」に差し替えて継続。
  • スキップする: 壊れたレコードは捨てて、成功したものだけ流す。
  • Optional に包む → 成功だけ通す: 失敗は empty にして自然に除外。
  • 結果オブジェクト(成功/失敗)で保持: 失敗を記録し、最後にレポート。
  • 再試行(リトライ)する: 一時的な失敗に限定して、回数・待機付きで再試行。
  • 致命的は早期に中断: データ破損や設計上あり得ない状態は RuntimeException へラップして外へ。

例題で理解する(よくある現場ケース)

例題1: 数値変換の失敗は「スキップ」

List<String> rows = List.of("10", "20", "x", "30");

List<Integer> ints = rows.stream()
    .map(s -> {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            System.err.println("変換失敗: '" + s + "'");
            return Optional.<Integer>empty();
        }
    })
    .flatMap(Optional::stream) // 成功だけ流す
    .toList();

System.out.println(ints); // [10, 20, 30]
Java
  • ポイント: ログは残しつつ、失敗行は除外。後段は「正しい整数」だけ扱える。

例題2: ファイル読み込みの失敗は「デフォルト値」で回復

List<Path> files = List.of(Paths.get("a.txt"), Paths.get("b.txt"));

List<String> contents = files.stream()
    .map(p -> {
        try {
            return Files.readString(p);
        } catch (IOException e) {
            System.err.println("読み込み失敗: " + p + " -> " + e.getMessage());
            return ""; // 空文字でフォールバック
        }
    })
    .toList();
Java
  • ポイント: レポート生成など「欠損を許容する」処理に向く。

例題3: 成功/失敗を後で集計する(結果オブジェクト)

record ReadResult<PathT, ValT>(PathT path, boolean ok, ValT value, Exception err) {}

List<Path> files = List.of(Paths.get("a.txt"), Paths.get("b.txt"));

List<ReadResult<Path, String>> results = files.stream()
    .map(p -> {
        try {
            return new ReadResult<>(p, true, Files.readString(p), null);
        } catch (IOException e) {
            return new ReadResult<>(p, false, null, e);
        }
    })
    .toList();

long ok = results.stream().filter(r -> r.ok()).count();
results.stream().filter(r -> !r.ok())
       .forEach(r -> System.err.println(r.path() + " -> " + r.err().getMessage()));
System.out.println("成功=" + ok + " / 失敗=" + (results.size() - ok));
Java
  • ポイント: 失敗を捨てずに「どこが、なぜ」失敗したかを可視化。

例題4: 一時的な失敗は「リトライ」(回数・待機付き)

static <T> T retry(int times, long sleepMs, java.util.concurrent.Callable<T> op) {
    Exception last = null;
    for (int i = 0; i < times; i++) {
        try { return op.call(); }
        catch (Exception e) {
            last = e;
            try { Thread.sleep(sleepMs); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
        }
    }
    throw new RuntimeException("Retry failed", last);
}

List<String> urls = List.of("https://example.com/a", "https://example.com/b");
List<String> bodies = urls.stream()
    .map(u -> {
        try {
            return retry(3, 200, () -> httpGet(u)); // 仮のHTTP関数
        } catch (RuntimeException e) {
            System.err.println("取得失敗(リトライ尽きた): " + u + " -> " + e.getMessage());
            return ""; // 最終的なフォールバック
        }
    })
    .toList();
Java
  • ポイント: 一時的エラー向け。リトライ回数や待機を必ず制限する。

テンプレート集(そのまま使える)

  • 失敗時デフォルト値
.map(x -> { try { return io(x); } catch (Exception e) { log(e); return defaultVal; } })
Java
  • 失敗をスキップ(Optional)
.map(x -> { try { return Optional.of(op(x)); } catch (Exception e) { log(e); return Optional.empty(); } })
.flatMap(Optional::stream)
Java
  • 結果オブジェクトで保持
record Result<T>(boolean ok, T value, Exception err) {}
.map(x -> { try { return new Result<>(true, op(x), null); }
            catch (Exception e) { return new Result<>(false, null, e); } })
Java
  • 致命的は Unchecked に再スロー
.map(x -> { try { return op(x); } catch (IOException e) { throw new UncheckedIOException(e); } })
Java
  • リトライユーティリティの適用
.map(x -> { try { return retry(3, 100, () -> op(x)); } catch (RuntimeException e) { return fallback; } })
Java

設計のコツと落とし穴

  • 回復か中断かを分ける: データ欠損ならフォールバック/再試行。仕様破綻(フォーマット不一致、整合性崩壊)は中断が妥当。
  • ログは残すが、過剰出力は避ける: 大量データではログがボトルネック。件数カウントとサマリだけにする選択肢も。
  • 並列ストリームで外部リストに書かない: 結果は collect や結果オブジェクトで安全に集約。副作用を排除。
  • Optional を活用して自然除外: 「失敗は empty」にして flatMap で成功だけ流せば後段がシンプルになる。
  • リトライは限定的に: 回数・待機・対象例外を絞る。恒常的な失敗には使わない。
  • 例外の原因は失わない: ラップして再スローする際は必ず原因例外を保持(例: new UncheckedIOException(e))。

まとめ

  • ストリームの例外リカバリは「デフォルト置換」「スキップ」「Optional」「結果保持」「限定リトライ」「致命的は中断」の組み合わせで設計する。
  • 後段が扱いやすい形(成功だけ流す/失敗を集計)へ整えると、処理が壊れにくく読みやすくなる。
  • ログとサマリで可観測性を担保しつつ、副作用を避けて安全に流すのが基本。

👉 練習課題: 「CSV 行のパースで失敗した行は Optional.empty にして除外」「もう一方は Result に残して最後に失敗件数と先頭5件のエラー内容を出力」——この2種類を実装して、要件に応じた fallback の使い分けを体感してください。

タイトルとURLをコピーしました