ストリームでの例外処理パターン(ラップ処理) — 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パターンを書き分けてみてください。どこまで回復したいかによって設計が変わる感覚がつかめます。
