我如何从Java 8 lambda内部抛出CHECKED异常,例如在流中使用?
换句话说,我想让代码像这样编译:
public List<Class> getClasses() throws ClassNotFoundException {
List<Class> classes =
Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.map(className -> Class.forName(className))
.collect(Collectors.toList());
return classes;
}
这段代码无法编译,因为上面的Class.forName()方法会抛出ClassNotFoundException,该异常会被检查。
请注意,我不想将已检查异常包装在运行时异常中,并抛出已包装的未检查异常。我想抛出检查异常本身,而不向流添加丑陋的try/catch。
只要使用Lombok的@SneakyThrows就可以了。
Christian Hujer已经详细解释了为什么严格来说,由于Java的限制,从流中抛出受控异常是不可能的。
其他一些回答解释了一些技巧,可以绕过语言的限制,但仍然能够满足抛出“检查异常本身,并且不向流中添加丑陋的try/catch”的要求,其中一些需要额外的数十行样板文件。
我要强调的是另一个选项,在我看来,这个选项比其他选项干净得多:Lombok的@SneakyThrows。这个问题在其他答案中也提到过,但有很多不必要的细节。
生成的代码非常简单:
public List<Class> getClasses() throws ClassNotFoundException {
List<Class> classes =
Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.map(className -> getClass(className))
.collect(Collectors.toList());
return classes;
}
@SneakyThrows // <= this is the only new code
private Class<?> getClass(String className) {
return Class.forName(className);
}
我们只需要一个提取方法重构(由IDE完成)和一行额外的@SneakyThrows。注释负责添加所有样板文件,以确保您可以抛出检查过的异常,而无需将其包装在RuntimeException中,也无需显式声明它。
你可以!
扩展@marcg的UtilException并在必要的地方添加抛出E:这样,编译器会要求你添加抛出子句,一切就像你可以在java 8的流上本机抛出检查异常一样。
说明:只需复制/粘贴LambdaExceptionUtil在您的IDE,然后使用它,如下面的LambdaExceptionUtilTest所示。
public final class LambdaExceptionUtil {
@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
void accept(T t) throws E;
}
@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
R apply(T t) throws E;
}
/**
* .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name))));
*/
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
return t -> {
try {
consumer.accept(t);
} catch (Exception exception) {
throwActualException(exception);
}
};
}
/**
* .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName))
*/
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
return t -> {
try {
return function.apply(t);
} catch (Exception exception) {
throwActualException(exception);
return null;
}
};
}
@SuppressWarnings("unchecked")
private static <E extends Exception> void throwActualException(Exception exception) throws E {
throw (E) exception;
}
}
一些测试显示使用和行为:
public class LambdaExceptionUtilTest {
@Test(expected = MyTestException.class)
public void testConsumer() throws MyTestException {
Stream.of((String)null).forEach(rethrowConsumer(s -> checkValue(s)));
}
private void checkValue(String value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
}
private class MyTestException extends Exception { }
@Test
public void testConsumerRaisingExceptionInTheMiddle() {
MyLongAccumulator accumulator = new MyLongAccumulator();
try {
Stream.of(2L, 3L, 4L, null, 5L).forEach(rethrowConsumer(s -> accumulator.add(s)));
fail();
} catch (MyTestException e) {
assertEquals(9L, accumulator.acc);
}
}
private class MyLongAccumulator {
private long acc = 0;
public void add(Long value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
acc += value;
}
}
@Test
public void testFunction() throws MyTestException {
List<Integer> sizes = Stream.of("ciao", "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
assertEquals(2, sizes.size());
assertEquals(4, sizes.get(0).intValue());
assertEquals(5, sizes.get(1).intValue());
}
private Integer transform(String value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
return value.length();
}
@Test(expected = MyTestException.class)
public void testFunctionRaisingException() throws MyTestException {
Stream.of("ciao", null, "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
}
}
您还可以编写一个包装器方法来包装未检查的异常,甚至使用表示另一个功能接口的附加参数(具有相同的返回类型R)来增强包装器。
在这种情况下,您可以传递一个函数,该函数将在异常情况下执行并返回。
请看下面的例子:
private void run() {
List<String> list = Stream.of(1, 2, 3, 4).map(wrapper(i ->
String.valueOf(++i / 0), i -> String.valueOf(++i))).collect(Collectors.toList());
System.out.println(list.toString());
}
private <T, R, E extends Exception> Function<T, R> wrapper(ThrowingFunction<T, R, E> function,
Function<T, R> onException) {
return i -> {
try {
return function.apply(i);
} catch (ArithmeticException e) {
System.out.println("Exception: " + i);
return onException.apply(i);
} catch (Exception e) {
System.out.println("Other: " + i);
return onException.apply(i);
}
};
}
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
处理map操作抛出的受控异常的唯一内置方法是将它们封装在CompletableFuture中。(如果你不需要保留异常,一个Optional是一个更简单的选择。)这些类旨在允许您以函数的方式表示偶然操作。
需要几个重要的帮助器方法,但是您可以得到相对简洁的代码,同时仍然明显地表明流的结果取决于映射操作是否成功完成。这是它的样子:
CompletableFuture<List<Class<?>>> classes =
Stream.of("java.lang.String", "java.lang.Integer", "java.lang.Double")
.map(MonadUtils.applyOrDie(Class::forName))
.map(cfc -> cfc.thenApply(Class::getSuperclass))
.collect(MonadUtils.cfCollector(ArrayList::new,
List::add,
(List<Class<?>> l1, List<Class<?>> l2) -> { l1.addAll(l2); return l1; },
x -> x));
classes.thenAccept(System.out::println)
.exceptionally(t -> { System.out.println("unable to get class: " + t); return null; });
这将产生以下输出:
[class java.lang.Object, class java.lang.Number, class java.lang.Number]
applyOrDie方法接受一个抛出异常的函数,并将其转换为一个返回已经完成的CompletableFuture的函数——要么用原始函数的结果正常完成,要么用抛出的异常异常完成。
第二个映射操作说明您现在得到了一个Stream<CompletableFuture<T>>,而不仅仅是一个Stream<T>。如果上游操作成功,CompletableFuture只负责执行该操作。API可以显式地做到这一点,但相对来说并不痛苦。
直到您到达收集阶段。这就是我们需要一个非常重要的助手方法的地方。我们希望“提升”一个正常的收集操作(在本例中是toList())在CompletableFuture——cfCollector()中,我们可以使用一个供应商、累加器、组合器和完成器来实现这一点,而不需要知道关于CompletableFuture的任何事情。
助手方法可以在GitHub上我的MonadUtils类中找到,这仍然是一项正在进行的工作。