1. Qiita
  2. 投稿
  3. Java

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

  • 12
    いいね
  • 5
    コメント

問題点:ラムダ式で例外を処理するのがダサい

Java8の Streams API 、使っていますか?とても便利ですね。
Streams API、というかラムダ式は非常に強力です。

ラムダ式は非常に強力だが例が貧弱.java
Function<String, String> slice = x -> x.substring(3, 6);
System.out.println(slice.apply("abcdefghij")); // -> "def"

が多く指摘されている通り1例外処理との相性が悪く、ラムダの外で例外を補足することはできません。(いや、そりゃラムダで一塊の処理なんだから当たり前でしょ、と思いつつ。)

上の例だと文字列が3文字以下だとエラーになってしまうので、エラー発生時にはそのまま返すように例外処理を入れてみましょう。

TryCatchがクールじゃないしネストが深くてやめてほしい.java
Function<String, String> sliceAndCatch = x -> {
    try {
        return x.substring(3, 6);
    } catch (Exception e) {
        return x;
    }
};

System.out.println(sliceAndCatch.apply("abcdefghij")); // -> "def"
System.out.println(sliceAndCatch.apply("xy")); // -> "xy"

わあ、ネストが深い。

また、検査例外を吐くメソッドの場合はラムダの中でTryCatchしなきゃいけないので、例外を投げるhoge::fugaに対してstream.map(hoge::fuga)で呼び出せず、もどかしい感じになってしまいます。
非検査例外なら呼び出すタイミングで取り出せます(try{stream.map(hoge::fuga)}catch{~})が、きれいではありません。
StreamやOptionalをラッパしたライブラリとかもありますが、割と不便です。

解決策:一行で書けるといいよね → こうすれば書けます

上の例くらいはワンライナーで書きたい。
2つラムダを渡して、1番目のラムダは正常系の処理、2番目のラムダで例外処理を書ければスマートな感じになりますよね。
というわけで、まずは例外が投げられる関数型インターフェースを用意して

例外が投げられるFunction.java
interface ThrowableFunction<T, R> {
    R apply(T t) throws Exception;
}

そして、こんな感じでTryを定義します

全貌が見えつつある.java
public <T, R> Function<T, R> Try(ThrowableFunction<T, R> onTry, BiFunction<Exception, T, R> onCatch) {
    return x -> {
        try {
            return onTry.apply(x);
        } catch (Exception e) {
            return onCatch.apply(e, x);
        }
    };
}

それだけでクールなコーディングが可能になります!

Cool.java
Function<String, String> coolSlice = Try(x -> x.substring(3, 6), (error, x) -> x);

System.out.println(coolSlice.apply("abcdefghij")); // -> "def"
System.out.println(coolSlice.apply("xy")); // -> "xy"

Consumerもおんなじようにすれば、

Consumerの場合.java
interface ThrowableConsumer<T> {
    void accept(T t) throws Exception;
}

public <T> Consumer<T> Try(ThrowableConsumer<T> onTry, BiConsumer<Exception, T> onCatch) {
    return x -> {
        try {
            onTry.accept(x);
        } catch (Exception t) {
            onCatch.accept(t, x);
        }
    };
}

こちらもクールに!

やったね.java
Consumer<String> coolCatch = Try(x -> {System.out.println(Integer.valueOf(x));}, (error, x) -> error.printStackTrace());

coolCatch.accept("33"); //-> 33
coolCatch.accept("ほげ"); //-> java.lang.NumberFormatException: For input string: "ほげ" ・・・

メリット/デメリット

この方法の良いところ

  • 短い
  • (検査例外をラップして非検査例外を投げて補足したりするのに比べて)ちゃんと例外を使い分けられる
  • ラップしたStreamやOptionalより汎用的。返ってくるのただのファンクションだし

よくないところ

  • 準備が長い → 他のラッパとかに比べたら軽量だし、パッと書いてstatic importしましょう
  • 関数型インターフェース全部にTry用意しなきゃいけない
  • 型安全じゃないし、拾いたくないRuntimeExceptionとかも拾ってしまう → なんかもっといい方法ないですかね。

応用:仕組みはシンプルだが役に立つ

もちろんメソッド参照でも渡せるので、こういう感じでもかける

普通の例.java
stream.flatMap(Try(this::calc, (x,y)->Stream.empty()));

あと別にStreamじゃなくてもFunctionを受け付けてくれるところなら、Optionalとかでも

Optional.java
Optional<String> name = getName();
name.flatMap(Try( dao::getFindByName ,(x,y)->Optional.empty()));

あとはインターフェースにラムダ渡す時も

自作インターフェース.java
interface JisakuInterface{
    String Method(String Argument);
}

JisakuInterface m = str->"hoge";
JisakuInterface f = Try(str -> str+str,(x,y)->"b")::apply;

と、メソッド参照にしてあげればラムダで簡単に渡せる。すばらしい。

あとはオレオレモナドとか作ったときに、

もなもどき.java
// LogicalDeletable.of(null) で論理削除を生成するようにする。
LogicalDeletable<LegacyData> record = getRecord(); 
// 変換にミスったやつは論理削除扱いにする
record.map(Try(Legacy::legacyTransform)) , (error,record) -> null));

みたいなので例外取れていい。

あと(4度目)例が思い浮かばないけど、スローされた内容はonCatchに引数で渡しているので、普通のTryCatchでできる機能は全部できるはず(try-with-resource以外は)。

そんなわけで皆さんもぜひかっこよく例外をキャッチしてください。