エンターテイメント!!

遊戯王好きのJavaエンジニアのブログ。バーニングソウルを会得する特訓中。

JavaでStream中の例外処理をなんとかしたい

きっかけ

個人プロダクトで、stream 中に検査例外が発生するケースに遭遇した。
stream 内に例外処理を書くと見た目が非常に不格好に感じられたため、なんとかできないか調査した。

`RuntimeException@ でラップして再Throwしようかとおもったけど、安易だなと思ったので辞めた。
自分がTypeScriptで開発してたときは、そのような方法を使わずに済んでいたため、別の手段があるのではと検討を開始した。

やったこと

JavaScriptPromise のように実行結果をラップする仕組みを作ることを考えた。
すでに、Kotlinが同様のことを実現できているとのことだったので、Kotlinのインタフェースを参考に実装した。

作ったファイル

クラスファイル 概要
Result.java 処理結果のラッパー
ThrowingRunnable.java 処理用関数インタフェース
ThrowingSupplier.java 処理用関数インタフェース

ThrowingRunnable / ThrowingSupplier は、当初 Throwable でハンドリングしようとした名残。
本当は、Exception~~にするべきだろうけど、面倒くさくて辞めた。

実際の実装

import java.util.function.Consumer;

public class Result<T> {
    private final T value;
    private final Exception exception;

    private Result(T value, Exception exception) {
        this.value = value;
        this.exception = exception;
    }

    public static <T> Result<T> success(T value) {
        return new Result<>(value, null);
    }

    public static <T> Result<T> failure(Exception exception) {
        return new Result<>(null, exception);
    }

    public static <T> Result<T> runCatching(ThrowingSupplier<T> supplier) {
        try {
            return success(supplier.get());
        } catch (Exception e) {
            return failure(e);
        }
    }

    public static Result<Void> runCatching(ThrowingRunnable runnable) {
        try {
            runnable.run();
            return success(null);
        } catch (Exception e) {
            return failure(e);
        }
    }

    public boolean isSuccess() {
        return exception == null;
    }

    public boolean isFailure() {
        return exception != null;
    }

    public T getOrNull() {
        return value;
    }

    public Exception getException() {
        return exception;
    }

    // 成功時の処理
    public Result<T> onSuccess(Consumer<T> action) {
        if (isSuccess()) {
            action.accept(value);
        }
        return this;
    }

    // 失敗時の処理
    public Result<T> onFailure(Consumer<Exception> action) {
        if (isFailure()) {
            action.accept(exception);
        }
        return this;
    }
}
@FunctionalInterface
public interface ThrowingRunnable {
    void run() throws Exception;
}
@FunctionalInterface
public interface ThrowingSupplier<T> {
    T get() throws Exception;
}

実装のポイント

  • 実行結果と例外情報を保持する Result がメインクラス。
  • 呼び出しは Result#runCatching が起点。
  • 成功時は onSuccess、失敗時は onFailure で処理を分岐。
    実装方針は Optional を参考にした。

使用例

以下は例外発生時に処理を終了し、ログを出すパターン。

allList.stream().map(feed -> Result.runCatching(() -> /*例外が発生する処理*/ ))
        .filter(Result::isFailure)
        .findFirst()
        .ifPresent(result -> logger.error("エラーが発生しました。", result.getException()));

全部処理したいなら、for-each使うとか、工夫次第で使い回せる。
streamの中間操作・終端操作をどう組み合わせるかがポイントになる(と思う)。

まとめ・振り返り

いい方法ないか考えたが、汎用性高くもたせるなら、これが正解だと思ってる。
個人的には、例外を補足する箇所と、例外を処理する箇所は分けて、この例外が発生したらどうするのパターンは、switch 式とか使えばいいんじゃね?って思ってる。
(※switch は普段好まないが)

Throwable を最初補足しようかと思ったが、検査例外への対応が主目的なので、Exception に変更した。クラス名変えたほうが良かったかも知れない。
stream の使い方に自信がないので、もっとスマートに解決できる方法がある気がする。
Github copilotと相談して作ったので、なにか盲点がありそうな気がしてる。
今の俺の頭だと思いつかないが。

Kotlinの例外処理について調べたが、検査例外という概念がないことに驚いた。
TypescriptとかJavascript触っていると、たしかに検査例外ない方が実装がシンプルになっている実感がある。

調査・開発するにあたって、Vavrってライブラリが何回か目に入って来た。
サンプルを少し見たが、ネストがどうしても深くなってしまう気がする。※今回の自分の実装もそうかも知れないが。。。
scalaに触発されたっぽいので、scalaの概念がわからないのと厳しいのかも知れない。
ユーザーの関数型プログラミングの習熟度に影響ありそうな気がする。
調査は別の機会で。

参考サイト

Java8のStreamやOptionalでクールに例外を処理する方法 #java8 - Qiita

Java Streams API を使って例外処理をきっちり行なうコードを書くことは難しい #java8 - Qiita

Java の lambda 内のチェック例外のウザさをなんとかする · GitHub