问题提出
昨天写代码的时候遇到了一个需要在Java8流中抛出异常的问题,结果一直编译报错,先看下代码:
//编译通过
Stream.of("a", "b", "c").forEach(str -> {
throw new RuntimeException();
});
//编译失败
Stream.of("a", "b", "c").forEach(str -> {
throw new IOException();
});
把异常catch住处理的场景就不说了,这里要讨论的是如果希望把异常向上抛的时候该怎么做?
问题分析
从上面的代码片段可以看出,RuntimeException是可以编译通过的,而IOException不行,因此先看下Java中关于异常的分类。
首先Java中的异常分为两类,受检查异常(Checked Exception)跟非受检异常(UnChecked Exception)。
受检异常表示代码中必须显示的处理该异常,比如try-catch或者在方法声明上加入throws关键字把异常向上抛出。
非受检异常也被称作运行时异常,就像我们常用的RuntimeException,表示代码中可以不进行处理,直白点说就是Java认为这是人为导致的问题,也是可以人为的在编写代码阶段就避免掉的问题。
由Error跟RuntimeException派生出来的类都是非受检异常,也就是运行时异常。其他的异常则问受检异常,主要就是IOException及其子类。
回到上面的代码片段,第一个之所以编译通过就是因为它抛出的是非受检异常,只有运行中才会处理,而编译器在编译阶段并不关心,因此没有问题。
第二段代码抛出的是IOException,它是一个受检异常,也就是说编译器强制要求开发人员在编译阶段就要显示的处理该异常,不能留给运行阶段,因此编译不通过。
理清楚问题以后,再来看下如何解决。上面已经说到显示处理异常不外乎两种方式,一种是使用try-catch把异常吃掉,还有一种就是不处理异常,把它向上抛,但此时需要在方法声明上面显示使用关键字throws处理,看下代码:
public static void main(String[] args) throws IOException{
Stream.of("a", "b", "c").forEach(str -> {
throw new IOException();
});
}
很遗憾你会发现这种方式依旧无法通过编译检查。这是为什么呢?我们使用lambda表达式其实是实现了一个接口(虽然lambda本质上是使用invokedynamic指令实现,它并不是一个类)。Stream类中forEach方法的源码如下:
void forEach(Consumer<? super T> action);
再来看下Consumer类的源码(删除掉部分代码):
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
这里其实代码throw new IOException()是写在了accept方法中,按照我们上面说的需要在该方法的声明中加入throws关键字才可以,而我们上面的实现代码是在main函数中声明throws,因此无法通过编译。Consumer是Java的原生类无法进行修改,那该如何解决?
到现在我们可以得出结论,在Stream的forEach方法中如果抛出受检异常,那么我们必须要把它捕获吃掉,而不能抛给上一级。
那么问题来了,如果现在的需求就是要在forEach中抛出受检异常该怎么办呢?毫无疑问我们可以放弃lambda表达式而使用之前的迭代器模式解决,但下面要讨论的是如果在lambda中解决这个问题。
解决方案
通过上面的问题,我们知道抛出非受检是不需要显示处理的,也就是说我们可以在forEach中把受检异常包装成非受检异常再抛出,如下:
//编译通过
Stream.of("a", "b", "c").forEach(str -> {
throw new RuntimeException(new IOException());
});
这种方法虽然可以解决问题,但对于代码实在是丑陋,也破坏了Java异常的结构。
下面看下通过泛型的方式稍微优雅的解决这个问题的方法,直接上代码:
//编写一个泛型方法对异常进行包装
static <E extends Exception> void doThrow(Exception e) throws E {
throw (E)e;
}
//编译通过
Stream.of("a", "b", "c").forEach(str -> {
doThrow(new IOException());
});
简单解释一下,这里的原理是利用泛型把要抛出异常的类型隐藏起来了,从泛型方法的声明来看,编译器不能明确的知道抛出异常类型,反过来如果把该泛型方法修改为如下:
static <E extends IOException> void doThrow(Exception e) throws E {
throw (E)e;
}
这样的话编译器就明确的知道该方法会抛出一个受检异常,因此也无法通过编译。
后记
关于在lambda表达式里不能抛出受检异常的问题网上很多人都喷Oracle把事情搞砸了,并且这个问题早在2010年就开始引发讨论了,没想到居然到现在都没有修复。
附上stackOverFlow上面关于这个问题讨论的一个帖子:
https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8-streams