「Stream例外ラップ」は「チェック例外を“Streamで扱える形”に着替えさせる」技
Stream の map や forEach に渡すラムダは、基本的に「チェック例外を投げられない」制約があります。
でも業務では、ファイルI/O、DBアクセス、外部API呼び出しなど、「チェック例外を投げるメソッド」を Stream の中で呼びたい場面が山ほどあります。
そこで必要になるのが「Stream例外ラップ」です。
「チェック例外をそのまま投げられないなら、“RuntimeException に包む”などして、Stream の世界で扱える形に着替えさせる」イメージです。
なぜそのまま書けないのか:チェック例外とラムダの相性
典型的な「コンパイルが通らない」例
例えば、次のようなメソッドがあるとします。
String readLine(Path path) throws IOException {
return Files.readString(path);
}
Javaこれを Stream の map で呼びたくなります。
List<Path> paths = ...;
paths.stream()
.map(this::readLine) // コンパイルエラー
.forEach(System.out::println);
JavareadLine が IOException(チェック例外)を throws しているため、Function<T, R> を要求する map に、そのまま渡すことはできません。
ここでの重要ポイントは、
「Stream の関数型インターフェース(Function, Consumer など)は、チェック例外を throws できない」という仕様です。
だからこそ、「例外をどう扱うか」を自分で決めて、ラップする必要が出てきます。
一番シンプルなパターン:RuntimeException に包んで投げ直す
「失敗したらその場で落ちていい」場合
「ここでの I/O 失敗は致命的だから、もう処理を続けなくていい」という場面なら、
チェック例外を RuntimeException に包んで投げ直すのが一番シンプルです。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
public final class Throwing {
private Throwing() {}
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
Java使い方はこうです。
List<Path> paths = ...;
paths.stream()
.map(Throwing.wrap(p -> Files.readString(p))) // IOException を RuntimeException にラップ
.forEach(System.out::println);
Javaここでの重要ポイントは三つです。
一つ目は、「ThrowingFunction という“例外を投げてもいい関数型”を自分で定義している」ことです。apply が throws Exception になっているので、Files.readString のようなメソッドをそのまま書けます。
二つ目は、「wrap メソッドの中で try-catch し、チェック例外を RuntimeException に包んで投げ直している」ことです。
これにより、外側の map から見ると「例外を投げない Function」として扱えます。
三つ目は、「“ここで失敗したら全体を止める”というポリシーを、RuntimeException という形で明示している」ことです。
業務的に「この処理は成功して当然。失敗はバグか環境異常」と割り切れる場所では、このパターンが素直です。
「失敗した要素だけスキップしたい」パターン
例外を握りつぶしてログだけ出す
「一部の要素だけ失敗しても、全体は続けたい」という場面も多いです。
例えば、「100ファイル中、読めないファイルがあっても、読めた分だけ処理したい」など。
import java.util.Optional;
import java.util.function.Function;
public final class Throwing {
private Throwing() {}
public static <T, R> Function<T, Optional<R>> wrapToOptional(
ThrowingFunction<T, R> f
) {
return t -> {
try {
return Optional.ofNullable(f.apply(t));
} catch (Exception e) {
// ここでログ出力など
System.err.println("失敗: " + t + " : " + e.getMessage());
return Optional.empty();
}
};
}
}
Java使い方はこうです。
paths.stream()
.map(Throwing.wrapToOptional(p -> Files.readString(p)))
.flatMap(opt -> opt.stream()) // Optional.empty はスキップ
.forEach(System.out::println);
Javaここでの重要ポイントは三つです。
一つ目は、「例外が起きた要素を Optional.empty() に変換し、flatMap で自然に“落とす”ようにしている」ことです。
二つ目は、「例外を握りつぶすのではなく、その場でログを出している」ことです。
“どの要素で失敗したか”を残しておくことで、後から原因を追いやすくなります。
三つ目は、「“失敗した要素はスキップする”という業務ポリシーを、Optional と flatMap という形でコードに刻んでいる」ことです。
「結果+エラー情報を全部集めたい」パターン
成功・失敗を分けて集計する
「失敗した要素も含めて、どれが成功・どれが失敗かを後で分析したい」場合は、
結果と例外を包んだ専用クラスを作るのが分かりやすいです。
public final class Result<T> {
private final T value;
private final Exception error;
private Result(T value, Exception error) {
this.value = value;
this.error = error;
}
public static <T> Result<T> success(T value) {
return new Result<>(value, null);
}
public static <T> Result<T> failure(Exception e) {
return new Result<>(null, e);
}
public boolean isSuccess() { return error == null; }
public T getValue() { return value; }
public Exception getError(){ return error; }
}
Javaラップ関数はこう書けます。
public static <T, R> Function<T, Result<R>> wrapToResult(
ThrowingFunction<T, R> f
) {
return t -> {
try {
return Result.success(f.apply(t));
} catch (Exception e) {
return Result.failure(e);
}
};
}
Java使い方の例です。
List<Result<String>> results =
paths.stream()
.map(Throwing.wrapToResult(p -> Files.readString(p)))
.toList();
long successCount = results.stream().filter(Result::isSuccess).count();
long failureCount = results.size() - successCount;
Javaここでの重要ポイントは、
「例外を“落とす”のではなく、“データとして持ち運ぶ”設計にしている」ことです。
業務的に「失敗も含めてレポートしたい」「どのケースでどんなエラーが出たか集計したい」なら、
このパターンがとても役に立ちます。
どこまでラップするか:設計の線引き
「Streamの中で完結させるか」「外に投げるか」
Stream例外ラップで一番大事なのは、
「例外をどこで扱い切るか」を決めることです。
RuntimeException に包んで外に投げる
→ 呼び出し側(もっと外側)でまとめてハンドリングする設計。
Optional や Result に包んで Stream の中で処理する
→ 「失敗も含めて流れの中で扱う」設計。
どちらが正しい、という話ではなく、
「この処理は業務的にどう扱うべき失敗なのか?」を先に決めてから、
それに合ったラップ方法を選ぶのが筋の良い考え方です。
まとめ:Stream例外ラップで身につけてほしい感覚
Stream例外ラップは、
単に「チェック例外を誤魔化すテクニック」ではなく、
「例外の扱い方を、Stream の“流れ”に合わせて設計し直す技術」です。
チェック例外をそのまま投げられないので、「RuntimeException に包んで落とす」のか、
「Optional や Result に包んで流し続ける」のかを意識して選ぶ。ThrowingFunction のような“例外を投げてもいい関数型”を自分で定義し、wrap 系ユーティリティで「try-catch のパターン」を一箇所に閉じ込める。
「どこで例外を握り、どこで表に出すか」を、業務の意味とセットで決める。
あなたのコードのどこかに、
Stream の中に無理やり try-catch をねじ込んで、読みにくくなっている箇所があれば、
それを一度「Stream例外ラップユーティリティ」に置き換えられないか眺めてみてください。
その小さな整理が、
「例外処理も含めて“流れとして”設計できるエンジニア」への、
確かな一歩になります。
