commons-lang3のFailableが便利


先にまとめ

Java8で登場したLambda式を使うと、例外処理が煩雑になりがちだけど、Apache Commons Lang3(3.11以降)のFailableを使用すると以下のようにスッキリ書ける。

使用前

private static void deleteFiles(List<Path> paths) throws IOException {
    for (Path path : paths) {
        Files.deleteIfExists(path);
    }
}

使用後

private static void deleteFiles(List<Path> paths) throws IOException {
    paths.forEach(Failable.asConsumer(Files::deleteIfExists));
}

以下、ここに至る経緯。読み飛ばし可。

Streamを使うと途端に例外処理が面倒になるよね

Java8でLambda式が登場し、コードがなんかカッコよく書けるようになった。早速自分もLambdaしてみよう!モダンなプログラマーっぽくなろう!とやってみると、例外処理でハマる。

private static void deleteFiles(List<Path> paths) throws IOException {
    for (Path path : paths) {
        Files.deleteIfExists(path);
    }
}

これをLambdaで書いてみる。

private static void deleteFiles(List<Path> paths) throws IOException {
    paths.forEach(Files::deleteIfExists);
}

すると、Files.deleteIfExistsがthrowするIOExceptionが処理されない、とコンパイルエラーになってしまう。よく見るとConsumer.acceptにはthrows定義が無いので当然ではある。
従ってこれを以下のようにtry-catchIOExceptionをハンドルする必要がある。しかし、これでは発生したIOExceptionをメソッドの呼び出し元にthrowするというインターフェイスを崩してしまうことになる。(またなにより、Lambdaを使ってカッコよく、という当初の目的が達せられず、レガシーなJavaおじさんに戻ってしまうことになる。)

private static void deleteFiles(List<Path> paths) throws IOException {
    paths.forEach(t -> {
        try {
            Files.deleteIfExists(t);
        } catch (IOException e) {
            // handle e
        }
    });
}

ちなみに、インターフェイスを崩さないよう発生例外を送出するにはこのような実装が必要になり、レガシーJavaおじさんどころか、退化してしまっているんではないかと思われる。

private static void deleteFiles(List<Path> paths) throws IOException {
    try {
        paths.forEach(t -> {
            try {
                Files.deleteIfExists(t);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    } catch (UncheckedIOException e) {
        throw e.getCause();
    }
}

解決

世の中のみなさんはどうしているのだろう、と調べていたら、こんな事例が。
(ネタ元が見当たらない。海外の人の投稿だったと記憶。ConsumerではなくFunctionの例だったかも。)

public class RethrowFunctions {
    @FunctionalInterface
    public interface ConsumerWithExceptions<T, E extends Exception> {
        void accept(T t) throws E;
    }

    public static <T, E extends Exception> Consumer<T>
            rethrowConsumer(ConsumerWithExceptions<T, E> consumer) throws E {
       return t -> {
            try {
                consumer.accept(t);
            } catch (Exception exception) { // CS:IGNORE Exceptionのcatchは仕方ない
                throwAsUnchecked(exception);
            }
        };
    }

    private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
        throw (E) exception;
    }

これを使うと、以下のように書ける。これならインターフェイスも崩さず、そしてシンプル!

private static void deleteFiles(List<Path> paths) throws IOException {
    paths.forEach(RethrowFunctions.rethrowConsumer(Files::deleteIfExists));
}

という訳で同様の実装を以下のFunctionalInterfaceに対しても実装し、そのjavaファイルを必要なプロジェクトに配って回ったのである。
(配布方法がイマイチではあるが、いろいろと制約事項があり。)

  • java.lang.Runnable
  • java.util.function.Function
  • java.util.function.Predicate
  • java.util.function.Supplier
  • java.util.function.BiConsumer
  • java.util.function.BiFunction
  • java.util.function.BiPredicate

解決、その後

ある日、commons-lang3にFailableというものを発見。もしや?!と思ってみてみると、まさに前述のRethrowFunctionsと同じことを実現するものであった。commons-lang3であればすでに様々なプロジェクトで使用しているし汎用的なものなので、これからは自作javaファイルを配らずこちらを使うことにしよう。

というわけで、commons-lang3のFailableおすすめ。