如何从 Java 8 流中抛出 CHECKED 异常?

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

请注意,我不想将已检查的异常包装在运行时异常中,而是抛出包装的未检查异常。我想抛出检查的异常本身,并且不向流中添加丑陋的/。trycatches


答案 1

对你的问题的简单回答是:你不能,至少不能直接。这不是你的错。甲骨文搞砸了。他们坚持检查异常的概念,但在设计功能接口,流,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

函数接口的定义方式当前阻止编译器转发异常 - 没有声明可以告诉 if ,以及。Stream.map()Function.apply() throws EStream.map() throws 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();
    }   
}   

在我们希望看到被错过的情况下,但它实际上错过了 。throwSomeMoreIOExceptionException

这并不完美,因为类型推断似乎正在寻找单个类型,即使在异常情况下也是如此。因为类型推断需要单个类型,需要解析为公用的 和 ,即 。EsuperClassNotFoundExceptionIOExceptionException

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

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

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


答案 2

此帮助程序类允许您在 Java 流中使用任何已检查的异常,如下所示:LambdaExceptionUtil

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
      .map(rethrowFunction(Class::forName))
      .collect(Collectors.toList());

注意抛出 ,已选中。流本身也抛出 ,而不是一些包装未经检查的异常。Class::forNameClassNotFoundExceptionClassNotFoundException

public final class LambdaExceptionUtil {

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

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

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
    R apply(T t) throws E;
    }

@FunctionalInterface
public interface Supplier_WithExceptions<T, E extends Exception> {
    T get() throws E;
    }

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
    void run() throws E;
    }

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
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) { throwAsUnchecked(exception); }
        };
    }

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
    return (t, u) -> {
        try { biConsumer.accept(t, u); }
        catch (Exception exception) { throwAsUnchecked(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) { throwAsUnchecked(exception); return null; }
        };
    }

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
    return () -> {
        try { return function.get(); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
    {
    try { t.run(); }
    catch (Exception exception) { throwAsUnchecked(exception); }
    }

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
    {
    try { return supplier.get(); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
    try { return function.apply(t); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}

关于如何使用它的许多其他示例(静态导入后):LambdaExceptionUtil

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(System.out::println));
    }

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
    List<Class> classes1
          = Stream.of("Object", "Integer", "String")
                  .map(rethrowFunction(className -> Class.forName("java.lang." + className)))
                  .collect(Collectors.toList());

    List<Class> classes2
          = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                  .map(rethrowFunction(Class::forName))
                  .collect(Collectors.toList());
    }

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
    Collector.of(
          rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
          StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
    }

@Test    
public void test_uncheck_exception_thrown_by_method() {
    Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

    Class clazz2 = uncheck(Class::forName, "java.lang.String");
    }

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
    Class clazz3 = uncheck(Class::forName, "INVALID");
    }    

截至2015年11月更新在@PaoloC的帮助下,代码得到了改进,请检查下面的答案并投赞成票。他帮助解决了最后一个问题:现在编译器会要求你添加 throw 子句,一切都好像你可以在 Java 8 流上原生地抛出已检查的异常。


附注 1上述类的方法可以毫无畏惧地使用,并且可以在任何情况下使用rethrowLambdaExceptionUtil


注2:上述类的方法是奖励方法,如果您不想使用它们,可以安全地从类中删除它们。如果您确实使用了它们,请小心行事,而不是在了解以下用例,优点/缺点和限制之前:uncheckLambdaExceptionUtil

• 如果您调用的方法实际上永远不会引发它所声明的异常,则可以使用这些方法。例如:new String(byteArr, “UTF-8”) 抛出 UnsupportedEncodingException,但 Java 规范保证 UTF-8 始终存在。在这里,throws声明是一种麻烦,任何用最小的样板来沉默它的解决方案都是受欢迎的:uncheckString text = uncheck(() -> new String(byteArr, "UTF-8"));

• 如果您正在实现一个严格的接口,并且您无法选择添加 throws 声明,但抛出异常是完全合适的,则可以使用这些方法。包装异常只是为了获得抛出它的特权,会导致堆栈跟踪具有虚假异常,这些异常不会提供有关实际出错的信息。一个很好的例子是Runnable.run(),它不会引发任何检查的异常。uncheck

•在任何情况下,如果您决定使用这些方法,请注意在没有抛出子句的情况下抛出CHECKED异常的以下2个后果:1)调用代码将无法按名称捕获它(如果您尝试,编译器会说:异常永远不会在相应的try语句的正文中抛出)。它会冒泡,可能会被一些“catch Exception”或“catch Throwable”捕获到主程序循环中,这可能是你想要的。2)它违反了最小意外原则:它不再足以捕获,以确保捕获所有可能的异常。出于这个原因,我认为这不应该在框架代码中完成,而只能在你完全控制的业务代码中完成。uncheckRuntimeException

  • 引用:

推荐