Java
lambda
java8
exception
StreamAPI

Java8のラムダ式やStream APIでクールに例外を扱う

More than 1 year has passed since last update.

Java 8のCollectionやStream APIでは、Iteratableが実装されていて、for文で計算していた部分をラムダ式を使えるようになった。

ただし、ConsumerなどのFunctionalInterfaceでは例外がスローするように定義されていないので、検査例外(CheckedException)をスローすることができないことが、ラムダ式の欠点とされている。検査例外をコンパイル時にチェックされないランタイム例外にラップしたり握りつぶしてしまうのは例外処理の観点でよろしくない。。

QiitaStackoverflowにも解決策が提示されているが、検査例外を握りつぶすものであったり、せっかくのラムダが見通しが悪いコードになっていたり、関数インタフェースごとのヘビーな実装であったりいまいちである。Google先生によるとみんな悩んでいる/(´ω`;)\

Runtime Exceptionを補足してRethrow (なんてしたくない)

RuntimeをCheckExceptionにunwrapする。これはコードの見通しが悪くなってやりたくない…

try {
  Arrays.asList(1, 2, 3).forEach(e -> {
    try { 
      // checked exceptionを吐く何らかのビジネスロジック
      methodTrowingIOException(e.intValue());
    } catch(IOException ex) {
      // throw new UncheckedIOException(ex);      
      throw new RuntimeException(ex);
    }
  }));
} catch (RuntimeException re) {
  Throwable e = re.getCause();// 検査例外をスロー
  throw e;
}

UncheckedIOExceptionを使っても良いですが、unwrapするのが面倒だし、RuntimeExceptionを使うのとあまり変わりません。

Exceptionをthrows節にもつFunctionalInterfaceを定義する

@FunctionalInterface
public interface CheckedFunction<T, R> {
   R apply(T t) throws Exception;
}

void foo (CheckedFunction f) { ... }

ただし、これだとConsumerを前提としたforEachなどには利用できないので却下。

Sneaky throw イディオムを使って検査例外を扱う

Sneaky throwとは、Javaコンパイラの型検査の秘孔をついて、検査例外をコンパイル時にはRuntimeExceptionであると騙すというLombokで使われているテクニックである。この記事が詳しい。

@SuppressWarnings("unchecked")
@Nonnull
public static <E extends Throwable> void sneakyThrow(@Nonnull Throwable ex) throws E {
    throw (E) ex;
}

// IOExceptionを投げているのに、コンパイラがRuntimeExceptionであると騙される為に、throwsやcatchが必要ないが、実際にはIOExceptionがスローされる
@Test(expected = IOException.class)
public void testSneakyThrow() {
    Throwing.sneakyThrow(new IOException());
}

このイディオムを応用して次のようなConsumerを定義する。

import java.util.function.Consumer;

@FunctionalInterface
public interface ThrowingConsumer<T> extends Consumer<T> {

    @Override
    default void accept(final T e) {
        try {
            accept0(e);
        } catch (Throwable ex) {
            Throwing.sneakyThrow(ex);
        }
    }

    void accept0(T e) throws Throwable;
}

利用時は次のようにする。ラムダの受け取りにはConsumerインタフェースを継承した上記のThrowingConsumerを用いる。StackOverflowの回答の応用版である。

    @Test
    public void testThrowingConsumer() throws IOException {
        thrown.expect(IOException.class);
        thrown.expectMessage("i=3");

        Arrays.asList(1, 2, 3).forEach((ThrowingConsumer<Integer>) e -> {
            int i = e.intValue();
            if (i == 3) {
                throw new IOException("i=" + i);
            }
        });
    }

Rethrow staticメソッドを使ったより簡潔な表現

ThrowingConsumerだとforEachでのラムダ式をThrowingConsumerで受け取る部分が表現がやや煩雑であった。これをより簡潔にしたい。

public final class Throwing {

    private Throwing() {}

    @Nonnull
    public static <T> Consumer<T> rethrow(@Nonnull final ThrowingConsumer<T> consumer) {
        return consumer;
    }

    @SuppressWarnings("unchecked")
    @Nonnull
    public static <E extends Throwable> void sneakyThrow(@Nonnull Throwable ex) throws E {
        throw (E) ex;
    }
}

ラムダ式をrethrowメソッドでwrapするだけ。これが今のところの私のファイナルアンサー。

コンパイラはforEachではランタイム例外しか出ないと思い込んでいますので、呼び出し元でthrows IOExceptionするか、適切なタイミングでtry/catchしてください。

import static hivemall.utils.lambda.Throwing.rethrow;

    @Test
    public void testRethrow() throws IOException {
        thrown.expect(IOException.class);
        thrown.expectMessage("i=3");

        Arrays.asList(1, 2, 3).forEach(rethrow(e -> {
            int i = e.intValue();
            if (i == 3) {
                throw new IOException("i=" + i);
            }
        }));
    }

もっと良い方法があったら教えてください。