Java 8 Lambda式における例外

Java 8 Lambda式の例外

1. 概要

Java 8では、Lambda Expressionsは動作を簡潔に表現する方法を提供することにより、関数型プログラミングを容易にし始めました。 ただし、JDKによって提供されるFunctional Interfacesは例外をうまく処理しません。また、例外の処理に関しては、コードが冗長で面倒になります。

この記事では、ラムダ式を作成するときに例外を処理するいくつかの方法について説明します。

2. 未チェックの例外の処理

まず、例を挙げて問題を理解しましょう。

List<Integer>があり、このリストのすべての要素で定数、たとえば50を除算して、結果を出力します。

List integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));

この式は機能しますが、問題が1つあります。 リスト内の要素のいずれかが0の場合、ArithmeticException: / by zeroを取得します。 従来のtry-catchブロックを使用して、そのような例外をログに記録し、次の要素の実行を続行するように修正しましょう。

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        System.out.println(50 / i);
    } catch (ArithmeticException e) {
        System.err.println(
          "Arithmetic Exception occured : " + e.getMessage());
    }
});

try-catchを使用すると問題は解決しますが、Lambda Expressionの簡潔さが失われ、本来の小さな関数ではなくなります。

この問題に対処するために、a lambda wrapper for the lambda functionと書くことができます。 コードを見て、どのように機能するかを見てみましょう。

static Consumer lambdaWrapper(Consumer consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (ArithmeticException e) {
            System.err.println(
              "Arithmetic Exception occured : " + e.getMessage());
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

最初に、例外の処理を担当するラッパーメソッドを作成し、ラムダ式をパラメーターとしてこのメ​​ソッドに渡しました。

ラッパーメソッドは期待どおりに機能しますが、基本的にラムダ式からtry-catchブロックを削除して別のメソッドに移動し、実際に記述されるコードの行数を減らすことはないと主張するかもしれません。

これは、ラッパーが特定のユースケースに固有である場合に当てはまりますが、ジェネリックを使用してこのメ​​ソッドを改善し、さまざまな他のシナリオで使用できます。

static  Consumer
  consumerWrapper(Consumer consumer, Class clazz) {

    return i -> {
        try {
            consumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = clazz.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw ex;
            }
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
  consumerWrapper(
    i -> System.out.println(50 / i),
    ArithmeticException.class));

ご覧のとおり、ラッパーメソッドのこの反復では、ラムダ式とキャッチするExceptionのタイプの2つの引数を取ります。 このラムダラッパーは、Integersだけでなく、すべてのデータ型を処理でき、スーパークラスExceptionではなく、特定の種類の例外をキャッチします。

また、メソッドの名前をlambdaWrapperからconsumerWrapperに変更したことに注意してください。 これは、このメソッドがタイプConsumerFunctional Interfaceのラムダ式のみを処理するためです。 FunctionBiFunctionBiConsumerなどの他の機能インターフェイス用に同様のラッパーメソッドを記述できます。

3. チェックされた例外の処理

前のセクションの例を考えてみましょう。ただし、整数を分割してコンソールに出力する代わりに、ファイルに書き込みます。 このファイルへの書き込み操作はIOExceptionをスローします。

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

コンパイル時に、次のエラーが発生します。

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

IOExceptionはチェックされた例外であるため、処理する必要があります。 ここで2つのオプションがあります。例外をスローして別の場所で処理するか、ラムダ式を持つメソッド内で処理することができます。 それぞれを1つずつ見ていきましょう。

3.1. ラムダ式からチェック済み例外をスローする

ラムダ式が記述されているメソッド(この場合はmain)から例外をスローしてみましょう。

public static void main(String[] args) throws IOException {
    List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
    integers.forEach(i -> writeToFile(i));
}

それでも、コンパイル中に、未処理のIOExceptionと同じエラーが発生します。 これは、ラムダ式が匿名内部クラスに似ているためです。 この場合、ラムダ式はConsumer<T>インターフェースからのaccept(T t)メソッドの実装です。

mainから例外をスローしても何も起こりません。また、親インターフェイスのメソッドは例外をスローしないため、実装ではできません。

Consumer consumer = new Consumer() {

    @Override
    public void accept(Integer integer) throws Exception {
        writeToFile(integer);
    }
};

acceptメソッドの実装では例外をスローできないため、上記のコードはコンパイルされません。

最も簡単な方法は、try-catchを使用し、チェックされた例外をチェックされていない例外にラップして、再スローすることです。

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        writeToFile(i);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

このアプローチは、コードをコンパイルして実行しますが、前のセクションの未チェックの例外の場合の例と同じ問題があります。

例外をスローするだけなので、例外をスローできる独自のConsumer Functional Interfaceを作成し、それを使用してラッパーメソッドを作成する必要があります。 それをThrowingConsumer:と呼びましょう

@FunctionalInterface
public interface ThrowingConsumer {
    void accept(T t) throws E;
}
static  Consumer throwingConsumerWrapper(
  ThrowingConsumer throwingConsumer) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

これで、簡潔さを失わずに例外をスローできるラムダ式を作成できます。

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

3.2. Lambda式でチェックされた例外を処理する

この最後のセクション。 ラッパーを変更して、チェック済み例外を処理します。 ThrowingConsumerインターフェースはジェネリックを使用しているため、特定の例外を処理できます。

static  Consumer handlingConsumerWrapper(
  ThrowingConsumer throwingConsumer, Class exceptionClass) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = exceptionClass.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw new RuntimeException(ex);
            }
        }
    };
}

この例では、このラッパーを使用してIOExceptionのみを処理し、チェックされていない例外でラップすることにより、他のチェックされた例外をスローできます。

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
  i -> writeToFile(i), IOException.class));

未チェックの例外の場合と同様に、ThowingFunctionThrowingBiFunctionThrowingBiConsumerなどの他の機能インターフェイスの兄弟をスローします。 対応するラッパーメソッドと一緒に記述できます。

4. 結論

この記事では、ラッパーメソッドを使用して簡潔さを失うことなく、ラムダ式の特定の例外を処理する方法について説明しました。 また、JDKに存在する機能インターフェイスのスローの代替を作成して、チェック済み例外を未チェックの例外にラップすることによってスローするか、処理する方法を学びました。

別の方法はexplore the sneaky-throws hack.になります

関数型インターフェイスとラッパーメソッドの完全なソースコードはhereからダウンロードでき、テストクラスはhere, over on Githubからダウンロードできます。

すぐに使用できる実用的なソリューションを探している場合は、ThrowingFunctionプロジェクトを確認する価値があります。