Java 8:Lambda表达式中强制检查异常处理。为什么是强制性的,而不是可选的?

71

我正在使用Java 8中的新lambda特性,并发现Java 8提供的实践非常有用。然而,我正在考虑是否有一种好的方法来解决以下情况。假设您有一个对象池包装器,需要某种工厂来填充对象池,例如(使用java.lang.functions.Factory):

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(new Factory<Connection>() {
            @Override
            public Connection make() {
                try {
                    return DriverManager.getConnection(url);
                } catch ( SQLException ex ) {
                    throw new RuntimeException(ex);
                }
            }
        }, maxConnections);
    }

}

将函数式接口转换为lambda表达式后,上面的代码会变成这样:
public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new RuntimeException(ex);
            }
        }, maxConnections);
    }

}

确实不错,但是检查异常java.sql.SQLException需要在lambda内部使用try/catch块。在我们公司,长期以来我们使用两个接口:
  • IOut<T> 相当于 java.lang.functions.Factory;
  • 对于通常需要传播已检查异常的情况,有一个特殊的接口:interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }
IOut<T>IUnsafeOut<T>都应该在迁移到Java 8时删除,但是IUnsafeOut<T, E>没有完全匹配项。如果lambda表达式可以像未经检查的异常一样处理已检查的异常,就可以在上面的构造函数中简单地使用以下内容:
super(() -> DriverManager.getConnection(url), maxConnections);

看起来更加清晰了。我发现我可以重写ObjectPool超类以接受我们的IUnsafeOut<T>,但据我所知,Java 8尚未完成,因此可能会有一些更改,例如:

  • 实现类似于IUnsafeOut<T, E>的东西?(说实话,我认为这很糟糕 - 主题必须选择接受什么:要么是Factory,要么是无法具有兼容方法签名的“不安全工厂”)
  • 在lambda中简单地忽略已检查的异常,因此不需要IUnsafeOut<T, E>替代品?(为什么不呢?例如,另一个重要的变化:我使用的OpenJDK的javac现在不需要将变量和参数声明为final即可在匿名类[函数接口]或lambda表达式中捕获)

因此,问题通常是:是否有办法绕过lambda中的已检查异常,或者计划在Java 8最终发布之前在将来解决这个问题?


更新1

嗯,就我目前所了解的情况而言,似乎目前没有办法,尽管引用的文章是从2010年起的:Brian Goetz解释了Java中的异常透明度。如果在Java 8中没有太多变化,这可以被视为一个答案。此外,Brian表示interface ExceptionalCallable<V, E extends Exception>(我在我们的代码遗留中提到的IUnsafeOut<T, E extends Throwable>)几乎没用,我同意他的观点。

我还错过了其他什么吗?


3
若您关注Lambda API的发展,值得注意的是java.util.functions.Factory::make现在变成了java.util.function.Supplier::get。您可以在http://lambdadoc.net查看最新的API文档,该网站是[Lambda FAQ](http://lambdafaq.org)的子网站。 - Maurice Naftalin
@MauriceNaftalin,感谢您的评论。我目前使用的是OpenJDK 1.8.0-EA。 - Lyubomyr Shaydariv
1
一种方法在这篇博客中给出。 - Matt
3
您所提到的异常透明度文稿只是一个候选提案,经过更详细的检查后,后来被认为存在缺陷。 - Brian Goetz
@Matt,链接已经失效。 - kopelitsa
9个回答

49

我不确定我是否回答了你的问题,但你可以简单地使用类似这样的东西吗?

public final class SupplierUtils {
    private SupplierUtils() {
    }

    public static <T> Supplier<T> wrap(Callable<T> callable) {
        return () -> {
            try {
                return callable.call();
            }
            catch (RuntimeException e) {
                throw e;
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public JdbcConnectionPool(int maxConnections, String url) {
        super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections);
    }
}

3
谢谢!是的,您的方法似乎在当前Java 8功能方面是一个可能的解决方案(这也是马特通过Guava Throwables的帖子评论建议的http://java8blog.com/post/37385501926/fixing-checked-exceptions-in-java-8)。它似乎也是最优雅的解决方案,但我想您也同意,这有点啰嗦。Brian Goetz在2010年尝试使用类似于“可变类型参数”的东西来解决这个问题,也许Java会以某种方式解决这个问题,谁知道呢。 - Lyubomyr Shaydariv
11
另一个很好的受检异常滥用的例子。在这种情况下,将异常包装在RuntimeException中可能是必要的,但如果在外部未解除包装,则违背了受检异常的全部意义。请花时间阅读为什么有受检异常存在以及应该在什么情况下使用:https://dev59.com/NHVD5IYBdhLWcg3wTJrF#19061110 - Gili
17
@Gili 检查性异常是对程序员的滥用,而不是反过来。它们促进了样板代码、附带复杂性和直接的编程错误。它们打破了程序流程,尤其是因为try-catch是一个语句而不是表达式。包装异常既麻烦,又只是一种基本设计错误的权宜之计。 - Marko Topolnik
7
99%的实际程序逻辑需要无论是已检查还是未检查异常都能够打破当前工作单元,并统一在顶层异常障碍处进行处理。异常是程序正常逻辑的一部分的情况很少,程序员不容易忽视这类情况,且需要编译器告知。 - Marko Topolnik
3
@gili 我有丰富的开发经验,可以同时涉及到前后端。尽管两者之间略有不同,但所有异常情况和需要及早捕获的异常情况的比例仍然非常大。例如,在执行用户输入之前会对其进行验证,这包括检查文件是否存在,使用 file.exists() 进行检查,这样就可以避免出现意外的 FileNotFoundException 异常。 - Marko Topolnik
显示剩余20条评论

34
在lambda邮件列表中,这个问题已经充分讨论了。正如你所看到的,Brian Goetz建议你可以编写自己的组合器来替代它:

或者您可以编写自己的简单组合器:

static<T> Supplier<T> exceptionWrappingSupplier(Supplier<T> b) {
     return e -> {
         try { b.accept(e); }
         catch (Exception e) { throw new RuntimeException(e); }
     };
}

你可以一次编写它,用的时间比编写原始电子邮件的时间还要短。对于每种使用的SAM类型也是同样的道理。

我更愿意把这看作是“玻璃99%满”,而不是其他选择。并非所有问题都需要新的语言功能作为解决方案。(更不用说新的语言功能总是会引起新的问题。)

在那些日子里,消费者界面被称为Block。

我认为这与JB Nizet的答案相对应。

稍后Brian 解释了为什么是以这种方式设计的(问题的原因)

是的,您必须提供自己的异常SAM。但是然后Lambda转换将与它们一起正常工作。

EG讨论了关于此问题的其他语言和库支持,并最终认为这是一种坏的成本效益权衡。

基于库的解决方案会导致SAM类型增加2倍(异常与否),这与现有的原始特化组合爆炸产生严重的交互作用。

可用的基于语言的解决方案在复杂性/价值权衡方面处于劣势。尽管还有一些替代方案,我们将继续探索——尽管显然不是针对8,也可能不是针对9。

与此同时,您拥有完成您想要的工具。我知道你更喜欢我们为你提供最后一英里(其次,你的请求实际上是一个含蓄的请求,“为什么不放弃检查异常”),但我认为当前状态可以让你完成工作。


2
谢谢回复。坦白地说,我希望Lambda表达式能够支持检查异常,并且Lambda表达式的存在不会受到检查异常的限制,因为它们是纯粹由编译器驱动的特性。但是没有这样的运气。 - Lyubomyr Shaydariv
1
@LyubomyrShaydariv 我认为专家组在解决了几个设计问题后仍然无法满足需求、要求或限制,使事情变得困难。另外还有其他重要的问题,如缺乏值类型、类型擦除和受检异常。如果 Java 具备了第一个而缺少了其他两个,JDK 8 的设计将会截然不同。因此,我们都必须明白这是一个充满了权衡的难题,EG 必须在某一处划定界限并做出决策。这可能并不总是能够让我们满意,但肯定会有提出解决方案的方法。 - Edwin Dalorzo
是的,但缺乏值类型和真正的泛型是由于JVM遗留问题造成的,尽管在运行时不存在已检查异常,它们纯粹是编译器驱动的。Kotlin或Xtend没有已检查的异常,因此它们不强制捕获例如IOException。对我来说,仍然不清楚设计团队为什么不允许在lambda表达式中省略已检查的异常。如果有一个省略了已检查的异常的lambda表达式可能会破坏预期的示例场景,那将是很好的。 - Lyubomyr Shaydariv
3
一个功能性接口与其他接口一样。您可以通过lambda表达式或手动实现它。如果您在接口方法的实现中抛出了一个未声明抛出该异常的已检查异常,那么您正在破坏编程语言的语义。您正在抛出代码处理接口实现时不期望的异常。我认为这是不可行的。我需要更详细的例子才能理解您的建议。也许您应该为此创建另一个讨论主题。 - Edwin Dalorzo
2
好吧,目前我唯一看到的东西,如果 Lambda 表达式能够支持检查异常,那么就是: try { iDontHaveThrowsDeclared(x -> someIOOperation(x)) } catch ( IOException ) { ... } 将无法编译,因为在 catch 块中的 IOException 被知道是一个已检查的异常,而 iDontHaveThrowsDeclared 没有 throws 子句。所以是的,它会出问题。 - Lyubomyr Shaydariv
1
@EdwinDalorzo,对于用户来说,Lambda表达式就像它没有函数体一样,就好像它完全在Lambda表达式编写的上下文中运行。出于同样的原因,在方法之外的Lambda表达式上下文中处理异常似乎很自然。问题在于,实际执行可能发生在一个非常、非常晚的时间点,在完全不同的上下文中,可能作为API的一部分。我们不能期望API处理每个可以想象到的已检查异常。将其包装在未经检查的异常中似乎是适当的。 - YoYo

5
我们在公司内部开发了一个项目,帮助我们解决了这个问题。两个月前,我们决定公开发布。
以下是我们的成果:
@FunctionalInterface
public interface ThrowingFunction<T,R,E extends Throwable> {
R apply(T arg) throws E;

/**
 * @param <T> type
 * @param <E> checked exception
 * @return a function that accepts one argument and returns it as a value.
 */
static <T, E extends Exception> ThrowingFunction<T, T, E> identity() {
    return t -> t;
}

/**
 * @return a Function that returns the result of the given function as an Optional instance.
 * In case of a failure, empty Optional is returned
 */
static <T, R, E extends Exception> Function<T, Optional<R>> lifted(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.lift();
}

static <T, R, E extends Exception> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.uncheck();
}

default <V> ThrowingFunction<V, R, E> compose(final ThrowingFunction<? super V, ? extends T, E> before) {
    Objects.requireNonNull(before);

    return (V v) -> apply(before.apply(v));
}

default <V> ThrowingFunction<T, V, E> andThen(final ThrowingFunction<? super R, ? extends V, E> after) {
    Objects.requireNonNull(after);

    return (T t) -> after.apply(apply(t));
}

/**
 * @return a Function that returns the result as an Optional instance. In case of a failure, empty Optional is
 * returned
 */
default Function<T, Optional<R>> lift() {
    return t -> {
        try {
            return Optional.of(apply(t));
        } catch (Throwable e) {
            return Optional.empty();
        }
    };
}

/**
 * @return a new Function instance which wraps thrown checked exception instance into a RuntimeException
 */
default Function<T, R> uncheck() {
    return t -> {
        try {
            return apply(t);
        } catch (final Throwable e) {
            throw new WrappedException(e);
        }
    };
}

}

http://github.com/pivovarit/throwing-function


这实际上是完整的实施吗?你缺少任何使用示例。 - Al Ro

5

2015年9月:

你可以使用ET来完成这个任务。ET是一个小型的Java 8库,用于异常转换和翻译。

使用ET,你可以编写如下代码:

super(() -> et.withReturningTranslation(() -> DriverManager.getConnection(url)), maxConnections);

多行版本:

super(() -> {
  return et.withReturningTranslation(() -> DriverManager.getConnection(url));
}, maxConnections);

在此之前,您需要创建一个新的ExceptionTranslator实例:

ExceptionTranslator et = ET.newConfiguration().done();

这个实例是线程安全的,可以被多个组件共享。如果你想的话,你可以配置更具体的异常转换规则(例如 FooCheckedException -> BarRuntimeException)。如果没有其他规则可用,检查异常会自动转换为 RuntimeException

(免责声明: 我是 ET 的作者)


4

您是否考虑过使用RuntimeException(未检查的)包装类来将原始异常从lambda表达式中传递出去,然后将包装的异常强制转回其原始的已检查异常?

class WrappedSqlException extends RuntimeException {
    static final long serialVersionUID = 20130808044800000L;
    public WrappedSqlException(SQLException cause) { super(cause); }
    public SQLException getSqlException() { return (SQLException) getCause(); }
}

public ConnectionPool(int maxConnections, String url) throws SQLException {
    try {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new WrappedSqlException(ex);
            }
        }, maxConnections);
    } catch (WrappedSqlException wse) {
        throw wse.getSqlException();
    }
}

创建自己独特的类应该可以防止将另一个未经检查的异常误认为是你在lambda中包装的异常,即使该异常在你捕获并重新抛出之前在管道中某个地方被序列化了。
嗯...我唯一看到的问题是你正在构造函数中使用super()调用,而法律规定它必须是构造函数中的第一条语句。try算作前面的语句吗?在我的代码中,我已经做到了这一点(没有构造函数)。

谢谢您的回复。我真的很想避免在lambda表达式中显式使用try/catch,因为try/catch块看起来很丑,使得lambda表达式也变得丑陋。我猜至少有两个原因目前不能这样工作:1)Java本身的历史;2)一个使用具有throws调用的方法的lambda表达式应该被“自动”声明为"throws SomeEx",即使该方法没有声明任何throws或者它声明要抛出另一种类型的异常而不是可以从lambda传播的异常。 - Lyubomyr Shaydariv
如果它可以“吞噬”已检查的异常,那么它可能会破坏您在任何调用者外部捕获的已检查异常或未检查异常的期望。例如,您可能希望捕获IOException以处理当前非法代码,如list.forEach(p -> outputStream.write(p.hashCode()))write()引发IOException),但您可能无法这样做,因为Iterable.forEach()未声明为抛出IOException的方法,因此编译器无法知道这些意图。有时,已检查的异常确实是有害的... - Lyubomyr Shaydariv
我同意。提供第二组java.util.function类来声明它们抛出(已检查的)异常是过度设计。特别是因为我们需要每个带有函数参数的方法都有两个版本 - 一个抛出异常,一个不抛出异常。这在很大程度上解释了为什么Scala没有检查异常(只有未检查异常)。Java 8是Scala的lambda的副本,只是用->代替了=>。我想知道Martin Odersky在Sun工作时是否建议过任何这方面的内容?我希望Java 9包括大多数API类的不可变版本。 - GlenPeterson

1
你可以从lambda表达式中抛出异常,只需以“自己的方式”声明它们(这使它们不幸地无法在标准JDK代码中重复使用,但是嘿,我们尽力而为)。
@FunctionalInterface
public interface SupplierIOException {
   MyClass get() throws IOException;
}

或者更通用的版本:
public interface ThrowingSupplier<T, E extends Exception> {
  T get() throws E;
}

参考此处。还提到使用“sneakyThrow”来避免声明已检查的异常,但仍然抛出它们。这让我有点头痛,也许可以考虑其他选项。


1

Paguro提供了包装已检查异常的功能接口。我在你问问题几个月后开始着手处理它,所以你可能是它的灵感来源之一!

您会注意到,与Java 8中包含的43个接口相比,Paguro仅有4个功能接口。这是因为Paguro更喜欢泛型而不是原始类型。

Paguro的不可变集合内置了单通道转换(从Clojure复制)。这些转换大致相当于Clojure transducers或Java 8 streams,但它们接受包装已检查异常的功能接口。请参见:Paguro和Java 8 streams之间的差异


这是很酷的东西,很高兴能成为你灵感的一部分。:) 在发布三年后,我仍然无法完全拒绝标准函数接口的卓越双胞胎。我相当确定这只是因为我太懒了,不想再创建另一个业务逻辑接口。我通常不会将它们与流处理结合使用,并且我认为检查异常可以是跨层通信的一个很好的选择。 - Lyubomyr Shaydariv
1
我现在认为,那些年里,从设计的角度来看,我的最初问题是错误的:我为什么要让Supplier实例成为一个工厂?现在我认为,帖子中的那个脏构造函数应该委托类似于IConnectionFactory的东西,它声明要抛出SQLException,以清楚地显示这种接口的意图,这种接口未来可能更容易或更难扩展。 - Lyubomyr Shaydariv

1

以所描述的方式包装异常是不起作用的。我已经尝试过了,但我仍然会得到编译器错误,这实际上是根据规范:lambda表达式抛出异常,与方法参数的目标类型不兼容:Callable;call()不会抛出它,因此我无法将lambda表达式作为Callable传递。

因此基本上没有解决方案:我们被迫写样板文件。我们唯一能做的就是发表自己的意见,认为这需要修复。我认为规范不应该仅仅因为不兼容的抛出异常而盲目地丢弃目标类型:它应该随后检查在调用范围内是否捕获或声明为throws的不兼容抛出异常。 对于未内联的lambda表达式,我建议我们可以将它们标记为静默抛出已检查异常(“静默”是指编译器不检查,但运行时仍应捕获)。 让我们用=>来标记这些表达式,而不是-> 我知道这不是一个讨论站点,但既然这是问题的唯一解决方案,请让自己的声音被听到,让我们改变这个规范!


包装被描述成可以模仿没有检查异常的Supplier<T>而不是Callable<T>--这是一种替代方式,对于我面临的大多数情况都有效。我个人会改变lambda表达式/函数接口对于检查异常的行为。但我怀疑lambda-dev团队会同意为lambda改变这种行为。不幸的是,Java的检查异常是出于好意,但在实践中却是有害的。这类似于在每个方法中使用try/catch将检查异常包装成RuntimeException - Lyubomyr Shaydariv
其实有一种比描述的稍微复杂些但更为通用的方法可以对受检异常进行包装,详情请关注我的回答。但是我仍然认为,如果编译器能够更加努力地工作,并允许我们在Lambda表达式中抛出受检异常,那么我们的编程生活会更加美好。目前我们无法捕获Lambda表达式中特定的受检异常,迫使我们写出丑陋而繁琐的代码。这真是太可怕了! - Waldo auf der Springe
顺便说一下,你可以通过lambda-dev团队的邮件列表尝试建议简化检查异常处理的想法。如果他们批准了你的电子邮件(而不是想法本身)-- 你可能会了解到更改规范的利弊以及Brian Goetz的个人想法和思考。 - Lyubomyr Shaydariv
谢谢你的建议。实际上我已经尝试过了,但是这个建议完全被忽略了。关于检查异常所做的贡献的反应似乎并不太积极。尽管如此,我希望那些有这些担忧的人能够在还有时间修复它的时候发声。Java Lambda看起来非常好,这值得修复。 - Waldo auf der Springe

1

jOOλ是一个库,支持将各种功能接口抛出的已检查异常包装成等效的JDK功能接口。例如:

// Wraps the checked exception in an unchecked one:
Supplier<Class<?>> supplier = Unchecked.supplier(() -> Class.forName("com.example.X"));

// Re-throws the checked exception without compile time checking:
Supplier<Class<?>> supplier = Sneaky.supplier(() -> Class.forName("com.example.X"));

我在这篇博客文章中制作了更多的示例。 免责声明:我制作了jOOλ


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接