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


70

我正在使用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>代理吗?(为什么不这样做?例如,另一个重要的变化:我使用的OpenJDKjavac现在不需要将变量和参数声明为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
对于所有跟随lambdas API演进的人来说,值得注意的是java.util.functions.Factory :: make现在是java.util.function.Supplier :: get。您可以在[Lambda FAQ](lambdafaq.org)的子站点lambdadoc.net上查看API文档的最新版本
Maurice Naftalin 2012年

@MauriceNaftalin,感谢您的评论。我目前使用OpenJDK 1.8.0-EA。
Lyubomyr Shaydariv

1
一种方法是在给定的这个博客
马特

@Matt,感谢您的帖子链接!
Lyubomyr Shaydariv

3
您所引用的异常透明性文档只是一个候选提案,后来在进行了更详细的检查后被认为是有缺陷的。
Brian Goetz 2015年

Answers:


45

不确定我是否真的回答了您的问题,但是您不能简单地使用类似的内容吗?

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要素的术语可能的解决方案(这也被马特在通过番石榴将Throwable后评论所说java8blog.com/post/37385501926/...)。这似乎也是最优雅的解决方案,但是,我想你也同意,那是一个小样板。布莱恩·格茨(Brian Goetz)在2010年尝试使用可变参数类型参数之类的方法来解决此问题,而Java可能会以某种方式解决此问题,谁知道。
Lyubomyr Shaydariv

10
检查滥用异常的另一个很好的例子。在这种情况下,可能有必要在RuntimeException中包装异常,但是如果无法在外部再次对其进行包装,则会使检查异常的全部内容失效。请花时间阅读为什么存在检查的异常以及何时应使用它们:stackoverflow.com/a/19061110/14731
Gili 2014年

16
@Gili经过检查的异常是对程序员的滥用,反之亦然。它们会提高样板代码,偶然的复杂性和彻头彻尾的编程错误。它们破坏了程序流程,尤其是因为try-catch它是语句而不是表达式。包装例外是a)令人讨厌的事情,b)仅是hack,可以解决基本的错误设计。
Marko Topolnik 2014年

7
在现实生活中,有99%的程序逻辑需要检查和未检查的异常,才能打破当前的工作单元,并在顶级异常屏障处对其进行统一处理。那些异常是常规程序逻辑的一部分的罕见情况,程序员不会轻易错过,而是需要编译器告诉他。
Marko Topolnik 2014年

2
@Gili您的例子正是我提到的那种罕见情况。在服务器端,这种情况甚至更少。至于与C的比较,与错误代码不同的是,只有空的catch块才能明确地忽略异常。有趣的是,当开发人员被迫对已检查的异常执行某些操作时,就会发生这种情况。您说我不喜欢例外的说法听起来像是您在试图逗趣,但对您失去幽默感感到抱歉。
Marko Topolnik 2014年

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转换将与他们一起工作。

专家组讨论了针对此问题的其他语言和库支持,最后认为这是一个不好的成本/收益折衷方案。

基于库的解决方案导致SAM类型(异常与非异常)发生2倍的爆炸,这与现有组合爆炸的不良相互作用导致原始专业化。

可用的基于语言的解决方案是复杂性/价值折衷的失败者。尽管有一些替代解决方案,我们将继续探索-尽管显然不是针对8个,也可能不是针对9个。

同时,您拥有执行所需任务的工具。我得到您的青睐,我们愿意为您提供最后一英里(其次,您的请求实际上是“为什么您不已经放弃受检查的异常”的薄弱要求),但是我认为当前状态允许您完成工作。


2
谢谢回复。坦白地说,我希望lambda表达式将是checked-exceptions友好的,并且由于lambda纯粹是由编译器驱动的功能,因此lambda会传递所有经过检查的异常。但是没有运气。
Lyubomyr Shaydariv 2014年

1
@LyubomyrShaydariv我认为专家组在几个设计问题上苦苦挣扎。保持向后兼容性的需求,要求或约束使事情变得困难,然后还有其他重要问题,例如缺少值类型,类型擦除和检查异常。如果Java是第一个而其他两个都没有,那么JDK 8的设计将大不相同。因此,我们都必须理解,这是一个艰难的问题,需要进行很多折衷,并且EG必须在某处划界并做出决定。这可能并不总是令我们满意,但是肯定会有建议的解决方法。
Edwin Dalorzo 2014年

3
@LyubomyrShaydariv功能接口与其他任何接口一样。您可以通过lambda表达式或手动实现它。如果您在接口方法的实现中抛出一个检查后的异常,而该方法未声明抛出该异常,则您正在破坏编程语言的语义。您将抛出处理接口实现的代码所不希望的异常。我看不到还可以。我需要一个关于您的主张的更详尽的例子,以使其对我有意义。也许您应该为此做另一个讨论。
Edwin Dalorzo 2014年

2
好吧,所以,如果lambda表达式是checked-exceptions-friendly,那么我目前唯一看到的是:try { iDontHaveThrowsDeclared(x -> someIOOperation(x)) } catch ( IOException ) { ... }无法编译,因为IOException在该代码catch块中已知该代码为checked,而iDontHaveThrowsDeclared没有throws子句。是的,它坏了。
Lyubomyr Shaydariv 2014年

1
@EdwinDalorzo,对于用户来说,Lambda表达式就像没有函数体,就像它完全在编写Lambda表达式的上下文中运行一样。出于同样的原因,很自然地希望可以在lambda表达式之外的方法中在同一上下文中处理异常。问题在于,实际执行可能会在完全不同的上下文中发生的很晚得多的时刻发生,可能是API的一部分。我们不能期望API处理所有可能的检查异常。包装未检查的异常似乎是适当的。
YoYo 2014年

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算作一个先前的发言?我在自己的代码中(没有构造函数)进行此工作。


谢谢你的回复。我真的很想防止try/catch在lambda表达式中显式显示ing,因为try/catch块看起来很丑,所以lambda也很丑。我猜至少有两个原因导致它到目前为止无法运行:1)Java本身的历史;2)使用throws内部具有调用方法的lambda表达式调用的方法,可能应“自动”声明为“ throws SomeEx ”,即使该方法声明为不带任何throws声明或声明引发其他类型的异常而不是从lambda传播。
Lyubomyr Shaydariv13年

3)我认为,如果它可以“吞噬”检查的异常,那么它可能会破坏您对可能希望捕获到外部任何调用方的检查异常或非检查异常的期望。例如,您可能希望捕获IOException诸如list.forEach(p -> outputStream.write(p.hashCode()))write()throws IOException)之类的当前非法代码,但是您可能无法做到这一点,因为Iterable.forEach()未将其声明为方法throwing IOException,因此编译器无法了解这些意图。有时经过检查的异常真的很有害……
Lyubomyr Shaydariv13年

我同意。提供第二组java.util.function类来声明它们引发了(已检查的)Exception,这可能会过大。特别是因为我们需要每个带有函数参数的方法的两个版本-一个抛出,一个不抛出。这对于解释为什么Scala没有检查异常(只有未检查的异常)大有帮助。Java 8是Scala的lambda的副本,带有->而不是=>。我想知道马丁·奥德斯基(Martin Odersky)在Sun工作期间是否提出了任何建议?我希望Java 9包含大多数API类的不可变版本。
2013年

3

我们在公司中开发了一个内部项目,对此提供了帮助。我们决定在两个月前公开上市。

这是我们想出的:

@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);
        }
    };
}

}

https://github.com/TouK/ThrowingFunction/


1

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

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


包装的描述就像它可以模仿更少的检查异常,Supplier<T>而不是Callable<T>-这是一种替代方法,它适用于我所遇到的大多数情况。我个人将更改lambda表达式/功能接口的行为以检查异常。但是我怀疑lambda-dev团队是否同意改变lambda的行为。不幸的是,Java检查的异常在意图上是好的,但实际上却是有害的。这类似于包装检查异常到RuntimeExceptiontry/catch的每一个方法。
Lyubomyr Shaydariv

好吧,有一种方法可以笼统地包装检查过的异常,该异常要比描述的更为雄辩,请参阅我即将发布的答案。但是,我仍然同意您的看法,如果编译器更加努力地工作,并允许我们在lambda中抛出检查异常,那么我们的编程寿命会更好。从目前的情况来看,我们被剥夺了在那里发生的特定检查异常,从而迫使我们进行难看而繁琐的编码。y!
Waldo auf der Springe

顺便说一句,您可以尝试通过lambda-dev小组的邮件列表向您推荐有关简化检查异常处理的想法。如果他们批准您的电子邮件(不是想法本身),那么您可能会知道他们在更改规范方面的利弊,也可能知道Brian Goetz的个人想法。
Lyubomyr Shaydariv

谢谢你的提示。我实际上尝试过,但是这个建议已被完全忽略。有关检查异常的贡献反应看起来不太积极。尽管如此,我希望那些有同样担忧的人能说出来,同时还有一些时间可以修复它。Java lambda看起来非常好,因此值得修复。
Waldo auf der Springe

1

Paguro提供了包装已检查的异常的功能接口。在您问了问题几个月后,我就开始研究它,因此您可能是其中的一部分!

您会注意到,Paguro中只有4个功能接口,而Java 8中包含43个接口。这是因为Paguro更喜欢泛型而不是基元。

Paguro在其不可变集合(从Clojure复制)中内置了单遍转换。这些转换大致等效于Clojure转换器或Java 8流,但是它们接受包含已检查异常的功能接口。请参阅:Paguro和Java 8流之间的区别


那是很酷的东西,很高兴能成为您灵感的一部分。:)发布后三年,我不能完全拒绝标准功能接口的杰出双胞胎。我敢肯定这只是因为我懒于创建另一个业务逻辑接口。我通常不会将它们与流处理结合使用,并且我认为检查异常可能是一个不错的选择,尤其是对于跨层通信而言。
Lyubomyr Shaydariv,2015年

1
我现在认为,在那些年里,从设计角度来看,我最初的问题是错误的:为什么我要一个Supplier实例完全成为工厂?现在,我认为帖子中的那个肮脏的构造函数应该只委派一个诸如a的IConnectionFactory声明,该对象声明SQLException要抛出的a只是为了清楚地揭示这种接口的意图,该接口将来可能会或多或少地容易地扩展。
Lyubomyr Shaydariv

1

可以从lambda中抛出它们,而只需将它们声明为“以您自己的方式”(不幸的是,这使它们无法在标准JDK代码中重复使用,但是,嘿,我们做到了。)

@FunctionalInterface
public interface SupplierIOException {
   MyClass get() throws IOException;
}

或更通用的版本:

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

ref这里。还提到了使用“ sneakyThrow”不声明已检查的异常,而是仍然将其抛出。这让我有点头疼,也许是一种选择。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.