我如何从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。


当前回答

这样做是不安全的。你可以作弊,但你的程序坏了,这将不可避免地反过来伤害别人(应该是你,但我们的作弊经常会伤害到别人)。

这里有一个稍微安全一点的方法(但我仍然不推荐这样做)。

class WrappedException extends RuntimeException {
    Throwable cause;

    WrappedException(Throwable cause) { this.cause = cause; }
}

static WrappedException throwWrapped(Throwable t) {
    throw new WrappedException(t);
}

try 
    source.stream()
          .filter(e -> { ... try { ... } catch (IOException e) { throwWrapped(e); } ... })
          ...
}
catch (WrappedException w) {
    throw (IOException) w.cause;
}

在这里,您要做的是捕获lambda中的异常,从流管道中抛出一个信号,表明计算异常失败,捕获信号,并对该信号进行操作以抛出底层异常。关键是您总是捕获合成异常,而不是在没有声明抛出异常的情况下允许已检查异常泄漏。

其他回答

只要使用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中,也无需显式声明它。

处理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类中找到,这仍然是一项正在进行的工作。

对于你的问题,简单的答案是:你不能,至少不能直接。这不是你的错。甲骨文搞砸了。他们坚持受控异常的概念,但在设计功能接口、流、lambda等时却总是忘记了受控异常。这对罗伯特·c·马丁(Robert C. Martin)等专家来说都是好消息,他把受控异常称为失败的实验。

在我看来,这是API中的一个大错误,而语言规范中的一个小错误。

API中的缺陷在于,它没有提供转发已检查异常的功能,而这实际上对函数式编程非常有意义。正如我将在下面演示的那样,这样的设施是很容易实现的。

语言规范中的错误在于,它不允许类型参数推断类型列表而不是单个类型,只要类型参数只在允许类型列表的情况下使用(throws子句)。

作为Java程序员,我们的期望是编译以下代码:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class CheckedStream {
    // List variant to demonstrate what we actually had before refactoring.
    public List<Class> getClasses(final List<String> names) throws ClassNotFoundException {
        final List<Class> classes = new ArrayList<>();
        for (final String name : names)
            classes.add(Class.forName(name));
        return classes;
    }

    // The Stream function which we want to compile.
    public Stream<Class> getClasses(final Stream<String> names) throws ClassNotFoundException {
        return names.map(Class::forName);
    }
}

然而,它给出:

cher@armor1:~/playground/Java/checkedStream$ javac CheckedStream.java 
CheckedStream.java:13: error: incompatible thrown types ClassNotFoundException in method reference
        return names.map(Class::forName);
                         ^
1 error

函数接口的定义方式目前阻止编译器转发异常——没有声明告诉Stream.map()如果Function.apply()抛出E, Stream.map()也会抛出E。

缺少的是用于传递已检查异常的类型参数的声明。下面的代码展示了如何使用当前语法声明这种传递类型参数。除了标记行中的特殊情况(这是下面讨论的限制),此代码将按预期进行编译和运行。

import java.io.IOException;
interface Function<T, R, E extends Throwable> {
    // Declare you throw E, whatever that is.
    R apply(T t) throws E;
}   

interface Stream<T> {
    // Pass through E, whatever mapper defined for E.
    <R, E extends Throwable> Stream<R> map(Function<? super T, ? extends R, E> mapper) throws E;
}   

class Main {
    public static void main(final String... args) throws ClassNotFoundException {
        final Stream<String> s = null;

        // Works: E is ClassNotFoundException.
        s.map(Class::forName);

        // Works: E is RuntimeException (probably).
        s.map(Main::convertClass);

        // Works: E is ClassNotFoundException.
        s.map(Main::throwSome);

        // Doesn't work: E is Exception.
        s.map(Main::throwSomeMore);  // error: unreported exception Exception; must be caught or declared to be thrown
    }   
    
    public static Class convertClass(final String s) {
        return Main.class;
    }   

    static class FooException extends ClassNotFoundException {}

    static class BarException extends ClassNotFoundException {}

    public static Class throwSome(final String s) throws FooException, BarException {
        throw new FooException();
    }   

    public static Class throwSomeMore(final String s) throws ClassNotFoundException, IOException  {
        throw new FooException();
    }   
}   

在throwSomeMore的情况下,我们希望IOException被错过,但它实际上错过了Exception。

这并不完美,因为类型推断似乎是在寻找单一类型,即使在异常的情况下也是如此。因为类型推断需要单一类型,E需要解析为ClassNotFoundException和IOException的公共超,即Exception。

需要对类型推断的定义进行调整,以便在允许使用类型列表(throws子句)的地方使用类型形参时,编译器可以查找多个类型。然后,编译器报告的异常类型将与引用方法的已检查异常的原始throws声明一样具体,而不是单一的全能超类型。

坏消息是,这意味着甲骨文搞砸了。当然,它们不会破坏用户土地代码,但是向现有功能接口引入异常类型参数会破坏显式使用这些接口的所有用户土地代码的编译。他们必须发明一些新的语法来解决这个问题。

更糟糕的消息是Brian Goetz在2010年就已经讨论过这个话题了(https://blogs.oracle.com/briangoetz/entry/exception_transparency_in_java, http://mail.openjdk.java.net/pipermail/lambda-dev/2010-June/001484.html),但我被告知这个调查最终没有成功,而且据我所知,Oracle目前没有减轻受控异常和lambdas之间交互的工作。

只要使用任何一个NoException(我的项目),jOOλ的Unchecked, throw -lambdas, Throwable接口或Faux Pas。

// NoException
stream.map(Exceptions.sneak().function(Class::forName));

// jOOλ
stream.map(Unchecked.function(Class::forName));

// throwing-lambdas
stream.map(Throwing.function(Class::forName).sneakyThrow());

// Throwable interfaces
stream.map(FunctionWithThrowable.aFunctionThatUnsafelyThrowsUnchecked(Class::forName));

// Faux Pas
stream.map(FauxPas.throwingFunction(Class::forName));

也许,更好、更实用的方法是包装异常并在流中进一步传播它们。以Vavr的Try类型为例。

例子:

interface CheckedFunction<I, O> {
    O apply(I i) throws Exception; }

static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
    return i -> {
        try {
            return f.apply(i);
        } catch(Exception ex) {

            throw new RuntimeException(ex);
        }
    } }

fileNamesToRead.map(unchecked(file -> Files.readAllLines(file)))

OR

@SuppressWarnings("unchecked")
private static <T, E extends Exception> T throwUnchecked(Exception e) throws E {
    throw (E) e;
}

static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
    return arg -> {
        try {
            return f.apply(arg);
        } catch(Exception ex) {
            return throwUnchecked(ex);
        }
    };
}

第二个实现避免将异常包装在RuntimeException中。throwUnchecked可以工作,因为在java中几乎所有泛型异常都被视为未检查的。