如何从Java 8流/ lambdas中抛出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
,这会被检查 .
请注意我不想将已检查的异常包装在运行时异常中,而是抛出包装的未经检查的异常 . I want to throw the checked exception itself ,并且没有将丑陋的 try
/ catches
添加到流中 .
15 回答
The simple answer to your question is: You can't, at least not directly. 这不是你的错 . 甲骨文搞砸了 . 他们坚持检查异常的概念,但在设计功能接口,流,lambda等时不一致地忘记处理已检查的异常 . 这对于像Robert C. Martin这样称为检查异常的专家来说是一个失败的实验 .
这实际上是API中的一个巨大错误,也是语言规范中的一个小错误 .
API中的错误是它没有提供转发已检查异常的工具,这实际上会对函数式编程产生很大的意义 . 正如我将在下面演示的那样,这样的设施很容易实现 .
语言规范中的错误是它不允许类型参数推断类型列表而不是单个类型,只要类型参数仅用于允许类型列表的情况(
throws
子句) .我们作为Java程序员的期望是以下代码应该编译:
但是,它给出了:
当前定义功能接口的方式阻止了编译器转发异常 - 没有声明如果
Function.apply() throws E
,Stream.map() throws E
也会告诉Stream.map()
.缺少的是用于传递已检查异常的类型参数的声明 . 以下代码显示了如何使用当前语法声明这样的传递类型参数 . 除了标记行中的特殊情况(这是下面讨论的限制)之外,此代码按预期编译和运行 .
在
throwSomeMore
的情况下,我们希望看到IOException
被遗漏,但它实际上错过了Exception
.这并不完美,因为即使在异常的情况下,类型推断似乎也在寻找单一类型 . 因为类型推断需要单个类型,
E
需要解析为super
和IOException
的共同super
,即Exception
.需要对类型推断的定义进行调整,以便如果在允许类型列表的情况下使用类型参数,编译器将查找多个类型(
throws
子句) . 然后,编译器报告的异常类型将与引用方法的已检查异常的原始throws
声明一样具体,而不是单个catch-all超类型 .坏消息是,这意味着Oracle搞砸了 . 当然它们不会破坏用户域代码,但是将异常类型参数引入现有的功能接口会破坏显式使用这些接口的所有用户域代码的编译 . 他们必须发明一些新的语法糖来解决这个问题 .
更糟糕的消息是,Brian Goetz在2010年https://blogs.oracle.com/briangoetz/entry/exception_transparency_in_java已经讨论过这个话题,似乎这个问题被忽略了,所以我想知道Oracle在做什么 .
这个
LambdaExceptionUtil
帮助器类允许您在Java流中使用任何已检查的异常,如下所示:注意
Class::forName
抛出ClassNotFoundException
,即 checked . 流本身也抛出ClassNotFoundException
,而不是一些包装未经检查的异常 .关于如何使用它的许多其他示例(在静态导入
LambdaExceptionUtil
之后):NOTE 1: 上面
LambdaExceptionUtil
类的rethrow
方法可以毫无顾虑地使用,并且是 OK to use in any situation . 非常感谢帮助解决最后一个问题的用户@PaoloC:现在编译器会要求你添加throw子句和所有东西,好像你可以在Java 8流上本地抛出已检查的异常 .NOTE 2: 上面
LambdaExceptionUtil
类的uncheck
方法是奖励方法,如果您不想使用它们,可以安全地从课程中删除它们 . 如果您确实使用过它们,请小心操作,而不是在了解以下用例,优点/缺点和限制之前:•如果要调用的方法实际上永远不会抛出它声明的异常,则可以使用
uncheck
方法 . 例如:new String(byteArr,"UTF-8")抛出UnsupportedEncodingException,但Java规范保证UTF-8始终存在 . 在这里,投掷声明是一个麻烦,任何解决方案,以最小的样板沉默它是受欢迎的:String text = uncheck(() -> new String(byteArr, "UTF-8"));
•如果您实现的是严格的接口,而您没有添加throws声明的选项,则可以使用
uncheck
方法,但抛出异常是完全合适的 . 仅仅包装一个例外获得抛出它的特权会导致带有虚假异常的堆栈跟踪,这些异常不会导致实际出错的信息 . 一个很好的例子是Runnable.run(),它不会抛出任何已检查的异常 .•在任何情况下,如果您决定使用
uncheck
方法,请注意在没有throws子句的情况下抛出CHECKED异常的这两个后果:1)调用代码将无法按名称捕获它(如果您尝试,编译器会说:异常永远不会抛出相应的try语句的主体) . 它会冒泡并可能被主程序循环中的一些"catch Exception"或"catch Throwable"捕获,这可能是你想要的 . 2)它违反了最不令人意外的原则:它将不再足以捕获RuntimeException
以保证捕获所有可能的异常 . 出于这个原因,我认为这不应该在框架代码中完成,而只能在您完全控制的业务代码中完成 .参考文献:
http://www.philandstuff.com/2012/04/28/sneakily-throwing-checked-exceptions.html
http://www.mail-archive.com/javaposse@googlegroups.com/msg05984.html
Project Lombok注释:@SneakyThrows
Brian Goetz的意见(反对):How can I throw CHECKED exceptions from inside Java 8 streams?
https://softwareengineering.stackexchange.com/questions/225931/workaround-for-java-checked-exceptions?newreg=ddf0dd15e8174af8ba52e091cf85688e *
你不能安全地做到这一点 . 你可以作弊,但是你的程序被打破了,这将不可避免地再次咬人(应该是你,但我们的作弊常常会打击别人 . )
这是一个稍微安全的方法(但我仍然不建议这样做 . )
在这里,您正在做的是捕获lambda中的异常,从流管道中抛出一个信号,指示计算异常失败,捕获信号,并对该信号进行操作以抛出基础异常 . 关键是你总是捕获合成异常,而不是允许检查异常泄漏而不声明抛出异常 .
您可以!
扩展@marcg的
UtilException
并在必要时添加throw E
:这样, the compiler will ask you to add throw clauses 就像你可以在java 8的流上本地抛出已检查的异常一样 .说明:只需在IDE中复制/粘贴
LambdaExceptionUtil
然后使用它,如下面的LambdaExceptionUtilTest
所示 .一些测试显示用法和行为:
只需使用NoException(我的项目),jOOλ's Unchecked,throwing-lambdas,Throwable interfaces或Faux Pas中的任何一个 .
我编写了a library,它扩展了Stream API,允许您抛出已检查的异常 . 它使用Brian Goetz的技巧 .
你的代码会变成
这个答案类似于17,但避免了包装器异常定义:
你不能 .
但是,您可能需要查看one of my projects,它允许您更轻松地操作此类"throwing lambdas" .
在您的情况下,您将能够这样做:
并 grab
MyException
.这是一个例子 . 另一个例子是你可以
.orReturn()
一些默认值 .请注意,这仍然是一项正在进行的工作,更多的是即将到来 . 更好的名字,更多的功能等
总结上面的注释,高级解决方案是使用特殊的包装器来实现未经检查的功能,使用类似API的构建器,它提供恢复,重新抛出和抑制 .
下面的代码演示了消费者,供应商和功能接口 . 它可以很容易地扩展 . 此示例删除了一些公共关键字 .
类 Try 是客户端代码的 endpoints . 安全方法可能具有每种功能类型的唯一名称 . CheckedConsumer , CheckedSupplier 和 CheckedFunction 是lib函数的检查类似物,可以独立于 Try 使用
CheckedBuilder 是用于处理某些已检查函数中的异常的接口 . 如果上一次失败, orTry 允许执行另一个相同类型的函数 . handle 提供异常处理,包括异常类型过滤 . 处理程序的顺序很重要 . 减少方法 unsafe 和 rethrow 重新抛出执行链中的最后一个异常 . 如果所有函数都失败,则减少方法 orElse 和 orElseGet 返回备用值,如可选值 . 还有方法 suppress . CheckedWrapper 是CheckedBuilder的常见实现 .
我使用这种包装异常:
它需要静态处理这些异常:
虽然在第一次
rethrow()
调用期间无论如何都会重新抛出异常(哦,Java泛型......),这种方式允许获得可能异常的严格静态定义(需要在throws
中声明它们) . 不需要instanceof
或其他东西 .TL;DR Just use Lombok's @SneakyThrows .
Christian Hujer已经详细解释了为什么由于Java的限制而严格地说,从流中抛出已检查的异常是不可能的 .
其他一些答案解释了解决语言局限性的技巧,但仍然能够满足抛出"the checked exception itself, and without adding ugly try/catches to the stream"的要求,其中一些需要额外的几行样板 .
我要强调另一个选择这样做,恕我直言比所有的更清洁其他人:龙目岛的
@SneakyThrows
. 它已经被其他答案所提及,但在很多不必要的细节下被埋没了 .生成的代码非常简单:
我们只需要一个
Extract Method
重构(由IDE完成)和一个额外的行@SneakyThrows
. 注释负责添加所有样板文件,以确保您可以抛出已检查的异常而不将其包装在_710163中,而无需明确声明它 .我同意上面的注释,在使用Stream.map时,您仅限于实现不抛出异常的Function .
但是,您可以创建自己的FunctionalInterface,如下所示抛出..
然后使用Lambdas或引用实现它,如下所示 .
处理
map
操作可以抛出的已检查异常的唯一内置方法是将它们封装在CompletableFuture中 . (如果您不需要保留异常,则Optional是一种更简单的替代方法 . )这些类旨在允许您以功能方式表示或有操作 .需要一些非平凡的辅助方法,但是您可以获得's relatively concise, while still making it apparent that your stream' s结果取决于
map
操作已成功完成的代码 . 这是它的样子:这会产生以下输出:
applyOrDie方法接受一个抛出异常的
Function
,并将其转换为Function
,返回已经完成的CompletableFuture
- 正常完成原始函数的结果,或者异常完成抛出异常 .第二个
map
操作说明你现在有一个Stream<CompletableFuture<T>>
而不是Stream<T>
. 如果上游操作成功,则CompletableFuture
仅负责执行此操作 . API使这个显而易见,但相对无痛 .直到你进入
collect
阶段,那就是 . 这是我们需要一个非常重要的辅助方法的地方 . 我们希望"lift"正常的集合操作(在这种情况下,toList()
)"inside"CompletableFuture
- cfCollector()让我们使用supplier
,accumulator
,combiner
和finisher
这样做,根本不需要了解CompletableFuture
.帮助器方法可以在我的MonadUtils类的GitHub上找到,这仍然是一项正在进行的工作 .
我认为这种方法是正确的:
将
Callable
中的已检查异常包含在UndeclaredThrowableException
中(这是此异常的用例)并将其展开到外部 .是的,我发现它很丑陋,我建议不要在这种情况下使用lambdas,只是回到一个好的旧循环,除非你正在使用并行流和paralellization带来客观的好处,证明代码的不可读性 .
正如许多其他人所指出的,这种情况有解决方案,我希望其中一个能够成为未来的Java版本 .
可能更好,更实用的方法是将异常包装起来并在流中进一步传播它们 . 例如,查看Try类型的Vavr .
例:
要么
第二个实现避免将异常包装在
RuntimeException
中 .throwUnchecked
有效,因为几乎所有的通用异常在java中都被视为未选中 .