Java Tips | コレクション:Stream例外ラップ

Java Java
スポンサーリンク

「Stream例外ラップ」は「チェック例外を“Streamで扱える形”に着替えさせる」技

Stream の mapforEach に渡すラムダは、基本的に「チェック例外を投げられない」制約があります。
でも業務では、ファイル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);
Java

readLineIOException(チェック例外)を 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 という“例外を投げてもいい関数型”を自分で定義している」ことです。
applythrows 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 で自然に“落とす”ようにしている」ことです。

二つ目は、「例外を握りつぶすのではなく、その場でログを出している」ことです。
“どの要素で失敗したか”を残しておくことで、後から原因を追いやすくなります。

三つ目は、「“失敗した要素はスキップする”という業務ポリシーを、OptionalflatMap という形でコードに刻んでいる」ことです。


「結果+エラー情報を全部集めたい」パターン

成功・失敗を分けて集計する

「失敗した要素も含めて、どれが成功・どれが失敗かを後で分析したい」場合は、
結果と例外を包んだ専用クラスを作るのが分かりやすいです。

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例外ラップユーティリティ」に置き換えられないか眺めてみてください。

その小さな整理が、
「例外処理も含めて“流れとして”設計できるエンジニア」への、
確かな一歩になります。

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