Java 逆引き集 | ストリームでの例外処理パターン(ラップ処理) — checked 例外の扱い

Java Java
スポンサーリンク

ストリームでの例外処理パターン(ラップ処理) — checked 例外の扱い

Stream のラムダは基本的に checked 例外を投げられません。ファイルやネットワークなどの I/O をストリームの中で扱うときは、例外を「ラップ」して安全に流す必要があります。初心者がつまずきやすい「どうやって checked を処理するか」を、よく使う4パターンに絞って丁寧に解説します。


よく使う4パターン(目的別の使い分け)

  • ラップして再スロー(Unchecked化):
    • ねらい: 例外を RuntimeException(例: UncheckedIOException)に包んで投げ直す。処理を中断し、外側でまとめて扱う。
  • その場で回復(デフォルト値・スキップ):
    • ねらい: 失敗した要素だけ無害化して流れを継続(デフォルト値、ログ、スキップ)。
  • 結果オブジェクトに包む(成功/失敗を後段で集計):
    • ねらい: 失敗を捨てずに「成功/失敗」を両方残して、最後に統計・レポートできる形にする。
  • Optional で失敗を自然に除外:
    • ねらい: 失敗時に Optional.empty() を返し、flatMap(Optional::stream) で成功だけを流す。

基本コード例(すぐ試せる)

1) ラップして再スロー(Unchecked 化)

import java.nio.file.*;
import java.io.UncheckedIOException;
import java.util.List;

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

List<String> contents = paths.stream()
    .map(p -> {
        try {
            return Files.readString(p); // Java 11+
        } catch (java.io.IOException e) {
            throw new UncheckedIOException(e); // 失敗は中断して外で扱う
        }
    })
    .toList();
Java
  • ポイント: ストリーム内で checked を扱えないので、UncheckedIOException に包むのが定番。

2) その場で回復(失敗はデフォルト値)

List<String> contents = paths.stream()
    .map(p -> {
        try {
            return Files.readString(p);
        } catch (java.io.IOException e) {
            System.err.println("読み込み失敗: " + p + " -> " + e.getMessage());
            return ""; // デフォルト値に置き換え(継続)
        }
    })
    .toList();
Java
  • ポイント: 失敗を「置き換え」で吸収。最終結果は常に得られる。

3) 結果オブジェクトで保持(成功/失敗の両方を後で利用)

record Result<T>(boolean ok, T value, Exception error) {}

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

long okCount = results.stream().filter(r -> r.ok()).count();
long ngCount = results.size() - okCount;
System.out.println("成功=" + okCount + " / 失敗=" + ngCount);
Java
  • ポイント: 失敗を捨てず、後で集計・ログ出力できる。

4) Optional で失敗を自然に除外

import java.util.Optional;

List<String> contents = paths.stream()
    .map(p -> {
        try {
            return Optional.of(Files.readString(p));
        } catch (java.io.IOException e) {
            return Optional.<String>empty();
        }
    })
    .flatMap(Optional::stream) // 成功だけが流れる
    .toList();
Java
  • ポイント: 失敗分はストリームから消える。後段は成功データだけを扱える。

例題で理解する(実務寄りケース)

例題1: CSV の各行を安全に整数へ変換(不正行はスキップ)

import java.nio.file.*;
import java.util.*;
import java.io.IOException;

Path csv = Paths.get("nums.csv");
try (var lines = Files.lines(csv)) {
    List<Integer> ints = lines
        .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);
}
Java
  • ポイント: 不正行を自然に除外。後段は「正しい整数」だけ。

例題2: 複数ファイルの読み込みを並列化+失敗は集計

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

record ReadStat(Path path, boolean ok, String msg) {}

List<ReadStat> stats = files.parallelStream()
    .map(p -> {
        try {
            var text = Files.readString(p);
            // ここでテキストを使った変換など
            return new ReadStat(p, true, "OK");
        } catch (java.io.IOException e) {
            return new ReadStat(p, false, e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    })
    .toList();

long ok = stats.stream().filter(s -> s.ok()).count();
stats.stream().filter(s -> !s.ok()).forEach(s -> System.err.println(s.path() + " -> " + s.msg()));
System.out.println("成功=" + ok + " / 失敗=" + (stats.size() - ok));
Java
  • ポイント: 並列でも外部共有リストに書かず、結果オブジェクトで安全に集計。

例題3: 変換関数を共通化(テンプレート化)

import java.util.function.Function;

// 失敗を Unchecked に包む共通関数
static <T, R> Function<T, R> unchecked(FunctionWithIO<T, R> f) {
    return t -> {
        try {
            return f.apply(t);
        } catch (java.io.IOException e) {
            throw new UncheckedIOException(e);
        }
    };
}

// checked 例外を投げる関数型
@FunctionalInterface
interface FunctionWithIO<T, R> {
    R apply(T t) throws java.io.IOException;
}

// 使い方
List<String> texts = paths.stream()
    .map(unchecked(Files::readString))
    .toList();
Java
  • ポイント: 例外ラップの繰り返しを「ひとつのユーティリティ」にまとめる。

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

  • UncheckedIOException で再スロー
.map(x -> { try { return ioOp(x); } catch (java.io.IOException e) { throw new UncheckedIOException(e); } })
Java
  • 失敗時にデフォルト値へフォールバック
.map(x -> { try { return ioOp(x); } catch (Exception e) { return defaultVal; } })
Java
  • Optional で失敗を除外
.map(x -> { try { return Optional.of(ioOp(x)); } catch (Exception e) { return Optional.empty(); } })
.flatMap(Optional::stream)
Java
  • 結果レコードで成功/失敗を保持
record Result<T>(boolean ok, T value, Exception err) {}
.map(x -> { try { return new Result<>(true, ioOp(x), null); } catch (Exception e) { return new Result<>(false, null, e); } })
Java
  • 共通ラッパ(関数化)
static <T, R> java.util.function.Function<T, R> unchecked(ThrowingFunction<T, R> f) {
    return t -> { try { return f.apply(t); } catch (Exception e) { throw new RuntimeException(e); } };
}
@FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; }
Java

落とし穴と回避策(本番で困らないために)

  • 例外握りつぶしによる原因不明:
    • 回避: 失敗を捨てるときも必ずログを残す。必要なら「失敗サマリ」を最後に出す。
  • スタックトレースの消失:
    • 回避: 再スロー時は必ず「原因例外」をラップし、throw new UncheckedIOException(e) のように原因を保持。
  • 外部状態への書き込み(副作用):
    • 回避: 並列ストリームで外部リストへ add しない。結果は collect や「結果レコード」で安全に集約。
  • 過剰な try-catch の散在:
    • 回避: ユーティリティ関数(unchecked ラッパ)にまとめると、読みやすく保守しやすい。
  • 全部 RuntimeException にする設計の乱用:
    • 回避: 「中断すべき致命的」か「回復可能」かで方針を分ける。致命的は再スロー、回復可能はフォールバックや Optional。
  • リソース管理忘れ:
    • 回避: Files.lines などは必ず try-with-resources。I/O は「閉じるまでが実装」。

まとめ

  • ストリームは checked 例外を直接投げられないため、「ラップ」か「回復」の方針を選ぶ。
  • Unchecked 化、フォールバック、結果オブジェクト、Optional の4パターンを使い分けると、現場の要件に柔軟に対応できる。
  • ユーティリティ化で重複を減らし、ログとスタックトレースを残しつつ、外部状態への副作用を避けるのが安全な設計。

👉 練習課題: 「複数ファイルを読み込み、失敗は Optional で除外したうえで文字数の合計を出す」「もう一つは失敗も Result に残して最後に失敗件数と例外種類の一覧を出す」——この2パターンを書き分けてみてください。どこまで回復したいかによって設計が変わる感覚がつかめます。

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