async 打包异常
Java 8已有两年历史,但是仍然存在社区尚未为其开发好的解决方案库的用例,甚至边缘用例。 如何处理流管道中的检查异常就是这样一个问题。 Stream操作接受的功能接口不允许实现抛出已检查的异常,但是我们可能要调用许多方法。 显然,这里存在一种紧张关系,许多开发人员都曾遇到过这种紧张关系。
我想在简短的系列文章中探讨这个主题:
- 重新打包流中的异常
- 重新打包异常以便抛出它们,而编译器不会抱怨它。 处理流中的异常
- 可以通过延迟错误处理来现场捕获并处理异常。 流中抛出异常
- 毕竟如何通过引发异常来处理延迟的错误。
我的主要目标是提出各种解决方案,并且在理想情况下,建立使讨论变得更容易的通用术语。 我还将对我的建议进行评论,并添加自己的评估意见-尽管这是次要的,但我希望它不会偏离主要目标:将想法付诸实践。
第一篇文章将研究重新打包异常,以便编译器停止抱怨。
设置场景
基本场景是流的每个频繁用户都遇到的一种或多种形式:您想在流的中间操作之一中使用的方法抛出一个已检查的异常。
在本文中,我将假定您正在尝试将字符串流解析为用户流:
Stream<User> parse(Stream<String> strings) {return strings.map(User::parse);
}
(如果您不打算将流作为参数或返回值,则假定整个流管道都在方法的范围内。以下方法适用于这两种方式,但是如果您在处理流上使用整个流,则某些评估会有所不同。点。)
不幸的是, User::parse
可以抛出ParseException
:
public class User {public static User parse(String userString) throws ParseException {// ...}}
这导致编译器抱怨方法参考User::parse
“未处理的异常:java.text.ParseException” 。 现在要做什么?
在我们研究此问题的解决方案之前,我想指出一点:我不认为Stream API与检查异常的不兼容性是可以通过其他设计克服的。 在某个时候,我可能会写一个更长的帖子来解释这一点,但是简短的版本是这样的:如果功能接口方法可以抛出检查的异常,那么将没有一种愉快的方式将其与流的惰性结合起来,因为它将是终端操作最终抛出该异常。
但是我们可以充分利用可以引发异常的函数,因此让我们在介绍该接口时对其进行介绍:
@FunctionalInterface
interface CheckedFunction<T, R, EX extends Exception> {R apply(T element) throws EX;}
这使我们可以将User::parse
分配给CheckedFunction<String
, User, ParseException>
。 请注意,异常的类型是通用的,稍后将派上用场。
重新打包流中的异常
那么,您真的必须处理例外情况吗? 不知道,您能不能解决问题? 令人惊讶的答案是“是的,您可以。” 是否应该拭目以待……
包装未检查的异常
给定一个引发检查异常的函数,将其转换为引发未检查异常的函数非常容易:
Stream<User> parse(Stream<String> strings) {return strings.map(uncheckException(User::parse))
}<T, R> Function<T, R> uncheckException(CheckedFunction<T, R, Exception> function) {return element -> {try {return function.apply(element);} catch (Exception ex) {// thanks to Christian Schneider for pointing out// that unchecked exceptions need not be wrapped againif (ex instanceof RuntimeException)throw (RuntimeException) ex;elsethrow new RuntimeException(ex);}};
}
这实际上还不错。 而且,无论如何,如果您更喜欢未检查的异常,那么这将更加诱人。 另一方面,如果您重视检查的异常(对于您期望的事情可能会出错,例如错误的输入)与未检查的异常(对于实现错误)之间的区别,那么这将使您不寒而栗。
在任何情况下,流的最终使用者都必须意识到可能会引发异常,这时需要与测试或文档进行通信,这两者都比编译器更容易忽略。 感觉就像在小河里藏了一颗炸弹。
最后,请注意,这会在第一个错误发生时立即中止流-可能会或可能不会发生的事情。 如果该方法返回一个流而不是使用它,则很难确定是否可行,因为不同的调用者可能有不同的要求。
偷偷摸摸的异常
解决整个问题的另一种方法是“偷偷地”抛出异常。 该技术使用泛型来混淆编译器,并使用@SuppressWarnings
使其剩余的投诉静音。
Stream<User> parse(Stream<String> strings) {return strings.map(hideException(User::parse));
}<T, R> Function<T, R> hideException(CheckedFunction<T, R, Exception> function) {return element -> {try {return function.apply(element);} catch (Exception ex) {return sneakyThrow(ex);}};
}@SuppressWarnings("unchecked")
<E extends Throwable, T> T sneakyThrow(Throwable t) throws E {throw (E) t;
}
嗯,什么? 如所承诺的, sneakyThrow
方法使用泛型来欺骗编译器以抛出未经检查的异常而不声明它。 然后hideException
使用它来捕获CheckedFunction
可能抛出的任何异常并CheckedFunction
将其重新抛出。 (如果您使用的是Lombok,请查看其@SneakyThrows
批注 。)
我认为这是非常冒险的举动。 一方面,它仍然在小河中隐藏着一颗炸弹。 但是,它进一步发展了,并使炸弹难以妥善化解。 您是否曾经尝试捕获未使用throws
子句声明的检查异常?
try {userStrings.stream().map(hideException(User::parse));.forEach(System.out::println);
// compile error because ParseException
// is not declared as being thrown
} catch (ParseException ex) {// handle exception
}
无法工作,因为编译器在没有方法实际抛出ParseException
的假设下运行。 相反,您必须捕获Exception
,过滤掉ParseException
并重新抛出其他所有内容。
哇,真烂!
不幸的是,这种技术在StackOverflow答案中得到了体现,在寻找Java流异常处理时,它在Google上的排名非常高。 公平地说,答案包含免责声明,但恐怕它可能会经常被忽略:
不用说,应该小心处理,项目中的每个人都必须意识到,未经声明的异常可能会出现在经过检查的异常中。
但是,正如我们已经看到的那样,没有很好的方法来声明/捕获这样的异常,因此我要说的是:
这是一个不错的实验,但从未真正做到! 如果确实要抛出,请包装运行时异常。
电梯例外
偷偷摸摸的问题是,这使流的消费者感到惊讶, 并且即使他们克服了这种惊讶,也很难处理该异常。 对于后者,至少有一个出路。 考虑以下功能:
<T, R, EX extends Exception> Function<T, R> liftException(CheckedFunction<T, R, EX> function) throws EX {return hideException(function);
}
它与hideException
完全相同, 但是声明它抛出EX。 为什么会有帮助? 因为可以通过这种方式使编译器理解可能会抛出检查异常:
Stream<User> parse(Stream<String> strings) {return strings// does not compile because `liftException`// throws ParseException but it is unhandled.map(liftException(User::parse));
}
问题是, liftException
的主体非常清楚地表明,它当然不会引发异常。 因此,在这样的示例中,我们仅看到管道的一部分,可以说使情况更加混乱。 现在,解析调用者可能会将其放入try-catch块中,期望能够很好地处理异常(如果不要对它太认真地考虑),然后当终端操作抛出该异常时仍会感到惊讶(记住它被sneakyThrow
)隐藏了。
但是,如果您是从不返回流的人, liftException
非常有用。 有了它,您的流管道中的一些调用就声明抛出一个已检查的异常,因此您可以将其全部放入try-catch块中:
try {userStrings.stream().map(liftException(User::parse));.forEach(System.out::println);
} catch (ParseException ex) {// handle exception
}
另外,包含管道的方法可以声明抛出异常:
List<User> parse(List<String> userStrings) throws ParseException {return userStrings.stream().map(liftException(User::parse));.collect(toList());
}
但是正如我之前所说,我认为只有在您永不返回流的情况下,此方法才有效。 因为如果这样做(即使只是偶尔这样做),则存在风险,即您或您的同事在重构期间可能会将管道拆开,从而使炸弹处于未声明的检查异常的状态,并隐藏在流中。
Sebastian Millies指出了另一个缺点,即到目前为止使用的接口和方法仅允许一个例外。 一旦一种方法声明了多个检查异常,事情就会成问题。 要么让Java派生一个公共的超类型(可能是Exception
), liftException
为一个以上的异常声明其他CheckedFunction
接口和liftException
方法。 两者都不是很好的选择。
给定抛出异常的方法,如果需要立即抛出异常,我向您展示了两种不同的方式在流中使用它们:
- 将检查的异常包装在运行时异常中
- 偷偷地抛出已检查的异常,以便编译器无法识别被抛出的异常
- 仍然偷偷摸摸地抛出,但是让utitility函数声明异常,以便编译器至少知道它被抛出了
请注意,所有这些方法都意味着流管线将在那里停止处理,除非产生副作用,否则不会产生任何结果。 我发现经常是不是我想做的事,但(因为我不喜欢返回物流)。 下一篇文章通过研究如何在不中断管道的情况下当场处理异常来解决此问题。
翻译自: https://www.javacodegeeks.com/2017/02/repackaging-exceptions-streams.html
async 打包异常