白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

ラムダ内の例外処理はもう少しおしゃれであってほしい

こんにちは。

今日から3連休ですね。
学生の皆様はもうそろそろ夏休みでしょうか?


さて、タイトルの通りなのですが、
Javaでは言語仕様上、ラムダ内での検査例外を投げることができないので、
せっかくのラムダが全然おしゃれじゃない感じになってしまいます。
(比較しづらそうなので、メソッド参照はあえて使っていません)

private void test() {
    
    // 非検査例外はOK
    Stream.of("a", "b", "c").forEach(str -> this.anythingActionThrowRuntime(str));
    
    // 検査例外はこうなる
    Stream.of("a", "b", "c").forEach(str -> {
        try {
            this.anythingActionThrow(str);
        } catch(Exception e) {
            // Runtimeで投げる?何もしない?
        }
    });
}

private void anythingActionThrowRuntime(String str) throws RuntimeException {
}

private void anythingActionThrow(String str) throws Exception {
}


まぁこれくらいは我慢してもいいかなとも思うのですが、
これが複数出てきたりするようになると…

private void test() {
    
    // 非検査例外はOK
    Stream.of("a", "b", "c").filter(str -> this.anythingFilterThrowRuntime(str))
                            .forEach(str -> this.anythingActionThrowRuntime(str));
    
    // 検査例外はこうなる
    Stream.of("a", "b", "c").filter(str -> {
                                try {
                                    return this.anythingFilterThrow(str);
                                } catch(Exception e) {
                                    // Runtimeで投げる?false返しておく?
                                }
                            })
                            .forEach(str -> {
                                try {
                                    this.anythingActionThrow(str);
                                } catch(Exception e) {
                                    // Runtimeで投げる?何もしない?                                
                                }
                            });
}

private boolean anythingFilterThrowRuntime(String str) throws RuntimeException {
    return true;
}

private boolean anythingFilterThrow(String str) throws Exception {
    return true;
}

もう無理ー。やだやだ。
私はfor文で書く!ってなってしまいます。


ぐずっていても仕方がないので、解決策を考えましょう。

上で見たとおり、非検査例外は投げられるので、
これを外側で拾うという前提のもとで(拾わなくてよいわけではない)、
検査例外を非検査例外にwrapするような仕組みにしておくのがなんだかよさそうな気がします。

FunctionalInterfaceな型を引数に受けるようにすると、ラムダは自動的にその型に変換してくれるので、

@FunctionalInterface
public static interface MyConsumer<T> {
    public void myAccept(T param);
}

public static <T> void accept(MyConsumer<T> consumer, T param) {
    consumer.myAccept(param);
}

こんな定義に対して、こんな風に呼ぶことができます。
(やっていることは普通のConsumerも何も変わりません)

accept(s -> System.out.println(s), "abc");


これを踏まえてとりあえず、Consumer、Supplier、Function、Predicateをwrapできるようにしてみます。

public class StreamHelper {
    
    /**
     * コンストラクタ
     */
    private StreamHelper() {
    }
    
    /**
     * ConsumerをThrowingにラップする
     */
    public static <T> Consumer<T> throwingConsumer(ThrowingConsumer<T> target) {
        return (param -> {
            try {
                target.accept(param);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ExceptionWrapper(e);
            }
        });
    }
    
    /**
     * SupplierをThrowingにラップする
     */
    public static <T> Supplier<T> throwingSupplier(ThrowingSupplier<T> target) {
        return (() -> {
            try {
                return target.get();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ExceptionWrapper(e);
            }
        });
    }
    
    /**
     * FunctionをThrowingにラップする
     */
    public static <T, R> Function<T, R> throwingFunction(ThrowingFunction<T, R> target) {
        return (arg -> {
            try {
                return target.apply(arg);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ExceptionWrapper(e);
            }
        });
    }
    
    /**
     * PredicateをThrowingにラップする
     */
    public static <T> Predicate<T> throwingPredicate(ThrowingPredicate<T> target) {
        return (arg -> {
            try {
                return target.test(arg);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ExceptionWrapper(e);
            }
        });
    }
    
    /**
     * ThrowingなConsumer
     */
    @FunctionalInterface
    public static interface ThrowingConsumer<T> {
        public void accept(T t) throws Exception;
    }

    /**
     * ThrowingなSupplier
     */    
    @FunctionalInterface
    public static interface ThrowingSupplier<T> {
        public T get() throws Exception;
    }
    
    /**
     * ThrowingなFunction
     */
    @FunctionalInterface
    public static interface ThrowingFunction<T, R> {
        public R apply(T arg) throws Exception;
    }
    
    /**
     * ThrowingなPredicate
     */
    @FunctionalInterface
    public static interface ThrowingPredicate<T> {
        public boolean test(T arg) throws Exception;
    }
    
    /**
     * ラムダの中で投げる未検査例外のWrapper
     */
    public static class ExceptionWrapper extends RuntimeException {
        private ExceptionWrapper(Throwable cause) {
            super(cause);
        }
    }
}


これを使うと一連の処理の外側でcatchして良いことになるので(本当の非検査例外は捕捉されない)、
わりとおしゃれにコードが書けそうですね。

try {
    Stream.of("a", "b", "c").filter(StreamHelper.throwingPredicate(str -> this.anythingFilterThrow(str)))
                            .forEach(StreamHelper.throwingConsumer(str -> this.anythingActionThrow(str)));
} catch(ExceptionWrapper e) {
    // なんか例外処理
}