2016-12-05
java8のlambdaの例外周りがうざい時のためのTry<T>について
インスパイアード [Javaの小枝] lambda式とチェック例外の相性の悪さをなんとかする - Qiita
Try<T>と言う考え方、僕の感覚だと散々既出って感じがするのですけど
かめくっぱさんがTryのLTしてくれるって信じてる
と言うアレがありつつ僕CCC行かなかったので需要があるのかどうか分からないですけどとりあえず書いてみます。
lambdaで例外周りがうざいとは
java8からlambda式と言う機能が追加されました*1が、FunctionalInterfaceのメソッドにthrows句が無いので牡蠣のコードがコンパイルエラーになると言うやつです。
public class Hoge { public static void main(String[] args) { List<String> strings = new ArrayList<>(); List<String> results = strings.stream() .map(Hoge::doSomething) .collect(Collectors.toList()); } private static String doSomething(String s) throws Exception { return s; } }
これを愚直になんとかしようとすると
List<String> results = strings.stream() .map(s -> { try { return doSomething(s); } catch (Exception e) { throw new RuntimeException(e); } }) .collect(Collectors.toList());
となり、これ毎回やるのクソだるくない?と言う話ですね。
じゃあどうしたらいいのか
とにかく現状の標準のFunctionalInterfaceは入力(引数)に対して出力(戻り値)が決まると言う感じのシグネチャになっていて、戻り値以外の出力(例外)があることが想定されていません。想定されていないというか、少なくともとにかく現状表現できない。
なら、エラー的情報も戻り値で表現できればええんちゃう?と言う発想がTry<T>型です。
別に奇特なアレコレというわけではなくて、皆さん既におなじみのOptional<T>型の親戚みたいなものです。
Optional<T>型は「値があるかもしれないし無いかもしれない」やつを表現する型ですが、一方Try<T>型は「値があるかもしれないし、エラーになったかもしれない」やつを表現する型です。Optionalと違うのは、ざっくり言ってしまえば「値がない状態」で、エラー情報を保持するようになったというところぐらいです。
例外が起こりうる処理をとにかくこのTry<T>と言うやつでラップしてしまえば少なくともラムダ式の中では例外がどうのという話を考えないで済みますし、例外のハンドリングに関して後回しに出来ます。と言っても分かる人とわからない人がいると思うのでそろそろ実際にやってみます。
作ってみる
まず下準備に、例外を投げても怒られないタイプのSupplierを作ります
@FunctionalInterface interface ExSupplier<T> { T get() throws Exception; }
次に
public abstract class Try<T> { public static <T> Try<T> of(ExSupplier<T> supplier){ // not implemented } }
と、とりあえずこんな感じのものが欲しいという気持ちだけ書きます。
こういう感じに書いて何がしたいかというと
Try<String> _try = Try<>.of(() -> doSomething("にゃーん"));
みたいに書いて例外を投げる系の処理をラップしてTry<T>型のインスタンスがほしいなと言う気持ちがあるわけです。
ですが、もちろん気持ちだけではコードは動かないのでちゃんと書いていく必要があります。
public abstract class Try<T> { public static <T> Try<T> of(ExSupplier<T> supplier){ try { return new Success<>(supplier.get()); } catch (Exception ex) { return new Failure<>(ex); } } private static final class Success<T> extends Try<T> { private final T value; private Success(T value) { this.value = value; } } private static final class Failure<T> extends Try<T> { private final Exception ex; private Failure(Exception ex) { this.ex = ex; } } }
一気に何やら増えましたが、やっていることはTry<T>のサブクラスとしてSuccess型とFailure型を作ったと言うだけです*2。
これらはそれぞれ処理が成功した場合を表す型、処理が失敗した場合を表す型です。これによって「Tryは処理が成功したか、あるいは失敗したかのどちらかを表す型」というのを表現しているわけです。必然的にSuccessはSupplierの戻り値を保持しますし、Failureは例外を保持します。
ので、Tryのofメソッドでは、Supplierが滞りなく終わればSuccessを、例外が飛んできたらFailureをそれぞれ構築して返すような実装になっていますね。
以上でTry<T>型の実装の肝は終わりです。多分大したことないと思います。たぶん。
使ってみる
で、Try型を導入したら最初のコードはどうなるんでしょうというのが下記
public class Hoge { public static void main(String[] args) { List<String> strings = new ArrayList<>(); List<Try<String>> results = strings.stream() .map(s -> Try.of(() -> doSomething(s))) .collect(Collectors.toList()); } private static String doSomething(String s) throws Exception { return s; } }
lambdaの中で例外がどうのと言う記述をしなくて良くなりました。
が、勘の良い方は気づいたと思いますが、なんとこのTry型、このままでは値を取得することが出来ません!!
さすがに処理結果がTryの向こう側に隔離されたままだと困るので何らかの方法で値を提供できないとだめです。
じゃあどうやって値を提供したらいいのかと言う話なんですが
- get()と言うメソッドを作って値を取得する。処理が失敗してた時は保持してた例外を投げる
- getOrRuntimeException()と言うメソッドを作って値を取得する。失敗した時は保持してた例外を実行時例外にラップしてスローする
- getWithRecover()と言うメソッドを作って、引数にはエラーハンドラ関数(例外を引数にとってT型を返す関数)を指定する。失敗してた時はその関数を使う
みたいな感じだと思います*3。
Optionalのget()とかgetOrElse()とかと似たような感じですね。
で、実装したのが下記
public abstract class Try<T> { public static <T> Try<T> of(ExSupplier<T> supplier){ try { return new Success<>(supplier.get()); } catch (Exception ex) { return new Failure<>(ex); } public abstract T get() throws Exception; public abstract T getOrWrappedRuntimeException(); public abstract T getWithRecover(Function<Exception, T> f); } private static final class Success<T> extends Try<T> { private final T value; private Success(T value) { this.value = value; } @Override public T get() throws Exception { return value; } @Override public T getOrWrappedRuntimeException() { return value; } @Override public T getWithRecover(Function<Exception, T> f) { return value; } } private static final class Failure<T> extends Try<T> { private final Exception ex; private Failure(Exception ex) { this.ex = ex; } @Override public T get() throws Exception { throw ex; } @Override public T getOrWrappedRuntimeException() { throw new RuntimeException(ex); } @Override public T getWithRecover(Function<Exception, T> f) { return f.apply(ex); } } }
で、再度使ってみると
public class Hoge { public static void main(String[] args) { List<String> strings = new ArrayList<>(); try { List<String> results = strings.stream() .map(s -> Try.of(() -> doSomething(s))) .map(Try::getOrWrappedRuntimeException) .collect(Collectors.toList()); } catch (RuntimeException ex) { // なんかエラー処理 } } private static String doSomething(String s) throws Exception { return s; } }
わかりやすくするため無駄にmap()を2回やってますが、まぁ、とにかくlambdaの中でtry-catchしなくて良くなりました。getOrWrapperdRuntimeException()メソッドを使っているので、1回目のmap()が例外になっていれば、2回目のmap()の時に実行事例外になって外へ飛んでいきます。
実行時例外にくるむとcatchの中でcause取って、さらに例外の型をinstanceofで確認しなきゃいけないみたいな泥臭い現実は解決できていないのでそれはそれで実際的には何かユーティリティ作る事になるとは思いますが*4、とにかくlambdaの中はやるべき処理に専念して、エラー処理は後回しにできるわけです。
Optionalと同じようにmap()メソッドやflatMap()メソッドを追加すれば、失敗する可能性のある処理をバンバンつなげることも出来ますね。
まとめ
飛び立とうとする例外がうざいならエラー情報も処理結果の一種として戻り値に含めて持ち回ったらいいよ
- 30 http://b.hatena.ne.jp/
- 21 https://t.co/P9Jlygr5xl
- 15 https://t.co/05c1mrh8yj
- 12 https://t.co/NAfwqj9VuM
- 10 https://www.google.co.jp/
- 7 http://www.google.co.uk/url?sa=t&source=web&cd=1
- 5 http://b.hatena.ne.jp/entrylist/it/技術ブログ
- 5 https://socialmediascanner.eset.com
- 4 http://labs.ceek.jp/hbnews/list.cgi
- 4 https://t.co/ve39Dxn2mK