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

  • 5
    Like
  • 0
    Comment

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

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

QiitaStackoverflowにもいくつか解決策が提示されているか、検査例外を握りつぶすものであったりいまいちである。

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 RuntimeException(ex);
    }
  }));
} catch (RuntimeException re) {
  Throwable e = re.getCause();
  throw e;
}

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するだけ。これが今のところの私のファイナルアンサー。

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);
            }
        }));
    }

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