Exceptions in Java 8 Lambda Expressions – Java 8 Lambda表达式中的异常情况

最后修改: 2017年 1月 17日

1. Overview

1.概述

In Java 8, Lambda Expressions started to facilitate functional programming by providing a concise way to express behavior. However, the Functional Interfaces provided by the JDK don’t deal with exceptions very well – and the code becomes verbose and cumbersome when it comes to handling them.

在Java 8中,Lambda表达式开始通过提供表达行为的简明方式来促进函数式编程。然而,JDK提供的功能接口并不能很好地处理异常 – 在处理异常时,代码变得冗长而繁琐。

In this article, we’ll explore some ways to deal with exceptions when writing lambda expressions.

在这篇文章中,我们将探讨一些在编写lambda表达式时处理异常的方法。

2. Handling Unchecked Exceptions

2.处理未检查的异常

First, let’s understand the problem with an example.

首先,让我们通过一个例子来了解这个问题。

We have a List<Integer> and we want to divide a constant, say 50 with every element of this list and print the results:

我们有一个List<Integer>,我们想用一个常数,比如说50除以这个列表中的每个元素,然后打印出结果。

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

This expression works but there’s one problem. If any of the elements in the list is 0, then we get an ArithmeticException: / by zero. Let’s fix that by using a traditional try-catch block such that we log any such exception and continue execution for next elements:

这个表达式有效,但有一个问题。如果列表中的任何元素是0,那么我们就会得到一个ArithmeticException:/ by zero。让我们通过使用传统的try-catch块来解决这个问题,这样我们就可以记录任何这样的异常并继续执行下一个元素。

List<Integer> 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());
    }
});

The use of try-catch solves the problem, but the conciseness of a Lambda Expression is lost and it’s no longer a small function as it’s supposed to be.

使用try-catch可以解决这个问题,但是Lambda Expression的简洁性被丢失了,它不再是一个应该有的小函数。

To deal with this problem, we can write a lambda wrapper for the lambda function. Let’s look at the code to see how it works:

为了处理这个问题,我们可以为lambda函数编写一个lambda包装器。让我们看一下代码,看看它是如何工作的。

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

At first, we wrote a wrapper method that will be responsible for handling the exception and then passed the lambda expression as a parameter to this method.

起初,我们写了一个负责处理异常的包装方法,然后把lambda表达式作为参数传给这个方法。

The wrapper method works as expected but, you may argue that it’s basically removing the try-catch block from lambda expression and moving it to another method and it doesn’t reduce the actual number of lines of code being written.

封装方法如期工作,但是,你可能会说,这基本上是把try-catch块从lambda表达式中移除,并把它移到另一个方法中,它并没有减少实际编写的代码行数。

This is true in this case where the wrapper is specific to a particular use case but we can make use of generics to improve this method and use it for a variety of other scenarios:

在这种情况下,包装器是针对特定用例的,这是真的,但我们可以利用泛型来改进这个方法,并将其用于其他各种情况。

static <T, E extends Exception> Consumer<T>
  consumerWrapper(Consumer<T> consumer, Class<E> 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<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
  consumerWrapper(
    i -> System.out.println(50 / i), 
    ArithmeticException.class));

As we can see, this iteration of our wrapper method takes two arguments, the lambda expression and the type of Exception to be caught. This lambda wrapper is capable of handling all data types, not just Integers, and catch any specific type of exception and not the superclass Exception.

正如我们所看到的,我们的封装方法的这个迭代需要两个参数,即lambda表达式和要捕获的Exception的类型。这个lambda包装器能够处理所有的数据类型,而不仅仅是Integers,并且能够捕捉任何特定类型的异常,而不是超类的Exception

Also, notice that we have changed the name of the method from lambdaWrapper to consumerWrapper. It’s because this method only handles lambda expressions for Functional Interface of type Consumer. We can write similar wrapper methods for other Functional Interfaces like Function, BiFunction, BiConsumer and so on.

另外,注意到我们把方法的名字从lambdaWrapper改为consumerWrapper。这是因为这个方法只处理Functional Interface类型Consumer的lambda表达式。我们可以为其他功能接口(如Function, BiFunction, BiConsumer等)编写类似的包装方法。

3. Handling Checked Exceptions

3.处理被检查的异常

Let’s modify the example from the previous section and instead of printing to the console, let’s write to a file.

让我们修改一下上一节的例子,不要打印到控制台,而是写到一个文件。

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}

Note that the above method may throw the IOException.

注意,上述方法可能会抛出IOException.

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

On compilation, we get the error:

编译时,我们得到了错误。

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

Because IOException is a checked exception, we must handle it explicitly. We have two options.

因为IOException是一个检查过的异常,我们必须明确地处理它。我们有两个选择。

First, we may simply throw the exception outside of our method and take care of it somewhere else.

首先,我们可以简单地在我们的方法之外抛出异常,并在其他地方处理它。

Alternatively, we can handle it inside the method that uses a lambda expression.

另外,我们可以在使用lambda表达式的方法里面处理。

Let’s explore both of the options.

让我们探讨一下这两种选择。

3.1. Throwing Checked Exception from Lambda Expressions

3.1.从Lambda表达式抛出检查过的异常

Let’s see what happens when we declare the IOException on the main method:

让我们看看当我们在main方法上声明IOException时会发生什么。

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

Still, we get the same error of unhandled IOException during the compilation.

但是,我们在编译过程中得到了同样的错误,即未处理的IOException

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

This is because lambda expressions are similar to Anonymous Inner Classes.

这是因为lambda表达式类似于匿名内部类

In our case, writeToFile method is the implementation of Consumer<Integer> functional interface.

在我们的例子中,writeToFile方法是Consumer<Integer>功能接口的实现

Let’s take a look at the Consumer‘s definition:

让我们看看Consumer的定义。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

As we can see accept method doesn’t declare any checked exception. This is why writeToFile isn’t allowed to throw the IOException.

我们可以看到accept方法没有声明任何检查的异常。这就是为什么writeToFile不允许抛出IOException.

The most straightforward way would be to use a try-catch block, wrap the checked exception into an unchecked exception and rethrow it:

最直接的方法是使用一个try-catch块,将被检查的异常包裹到一个未检查的异常中,然后重新抛出。

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

This gets the code to compile and run. However, this approach introduces the same issue we already discussed in the previous section – it’s verbose and cumbersome.

这样就可以获得代码的编译和运行。然而,这种方法引入了我们在上一节已经讨论过的同样的问题–它是冗长而繁琐的。

We can get better than that.

我们可以得到比这更好的结果。

Let’s create a custom functional interface with a single accept method that throws an exception.

让我们创建一个自定义的功能接口,它有一个抛出异常的accept方法。

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

And now, let’s implement a wrapper method that’s able to rethrow the exception:

现在,让我们实现一个能够重新抛出异常的包装方法。

static <T> Consumer<T> throwingConsumerWrapper(
  ThrowingConsumer<T, Exception> throwingConsumer) {
 
    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

Finally, we’re able to simplify the way we use the writeToFile method:

最后,我们能够简化我们使用writeToFile方法的方式。

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

This is still a kind of a workaround, but the end result looks pretty clean and is definitely easier to maintain.

这仍然是一种变通办法,但最终结果看起来相当干净,而且肯定更容易维护

Both, the ThrowingConsumer and the throwingConsumerWrapper are generic and can be easily reused in different places of our application.

ThrowingConsumerthrowingConsumerWrapper都是通用的,可以很容易地在我们应用程序的不同地方重复使用。

3.2. Handling a Checked Exception in Lambda Expression

3.2.在Lambda表达式中处理被检查的异常

In this final section, we’ll modify the wrapper to handle checked exceptions.

在最后一节,我们将修改包装器以处理检查过的异常。

Since our ThrowingConsumer interface uses generics, we can easily handle any specific exception.

由于我们的ThrowingConsumer接口使用了泛型,我们可以轻松地处理任何特定的异常。

static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(
  ThrowingConsumer<T, E> throwingConsumer, Class<E> 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);
            }
        }
    };
}

Let’s see how to use it in practice:

让我们看看如何在实践中使用它。

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

Note, that the above code handles only IOException, whereas any other kind of exception is rethrown as a RuntimeException.

注意,上述代码只处理IOException,而任何其他类型的异常都会作为RuntimeException被重新抛出。

4. Conclusion

4.结论

In this article, we showed how to handle a specific exception in lambda expression without losing the conciseness with the help of wrapper methods. We also learned how to write throwing alternatives for the Functional Interfaces present in JDK to either throw or handle a checked exception.

在这篇文章中,我们展示了如何在lambda表达式中处理一个特定的异常,并且在包装方法的帮助下不失其简洁性。我们还学习了如何为JDK中的功能接口编写抛出的替代品,以抛出或处理一个被检查的异常。

Another way would be to explore the sneaky-throws hack.

另一种方法是探索偷偷摸摸地抛出的黑客。

The complete source code of Functional Interface and wrapper methods can be downloaded from here and test classes from here, over on Github.

功能界面和封装方法的完整源代码可以从这里下载,测试类可以从这里下载,在Github

If you are looking for the out-of-the-box working solutions, ThrowingFunction project is worth checking out.

如果你正在寻找开箱即用的工作解决方案,ThrowingFunction项目值得一看。