这是称为“ Functional Java by Example”的系列文章的第3部分。
我在本系列的每个部分中发展的示例是某种“提要处理程序”,用于处理文档。 在前面的部分中,我从一些原始代码开始,并应用了一些重构来描述“什么”而不是“如何”。
为了帮助代码向前发展,我们需要摆脱良好的java.lang.Exception
。 (免责声明:我们实际上无法摆脱它)这就是其中的内容。
如果您是第一次来这里,最好从头开始阅读。 它有助于了解我们从何处开始以及如何在整个系列中继续前进。
这些都是这些部分:
- 第1部分–从命令式到声明式
- 第2部分–讲故事
- 第3部分–不要使用异常来控制流程
- 第4部分–首选不变性
- 第5部分–将I / O移到外部
- 第6部分–用作参数
- 第7部分–将失败也视为数据
- 第8部分–更多纯函数
我将在每篇文章发表时更新链接。 如果您通过内容联合组织来阅读本文,请查看我博客上的原始文章。
每次代码也被推送到这个GitHub项目 。
快速了解异常
自Java 1.0以来,我们的java.lang.Exception
一直存在-基本上在好时机和其他时候成为我们的敌人。
关于它们的讨论不多,但是如果您想阅读一些资源,这是我的最爱:
- Java异常 (JavaWorld)
- Java例外– GeeksforGeeks (geeksforgeeks.org)
- 9种处理Java中异常的最佳实践 (stackify.com)
- 异常处理的最佳实践 (onjava.com)
- Java异常面试问答 (journaldev.com)
- 带有示例的Java中的异常处理 (beginnersbook.com)
- Java异常处理(Try-catch) (hackerrank.com)
- 十大Java异常处理最佳实践– HowToDoInJava (howtodoinjava.com)
- Java中的异常处理和断言– NTU (ntu.edu.sg)
- 异常处理:最佳实践指南 (dzone.com)
- 在Java中处理异常的9种最佳实践 (dzone.com)
- 修复7个常见Java异常处理错误 (dzone.com)
- Java惯例->已检查异常与未检查异常 (javapractices.com)
- Java中异常的常见错误 MikaelStåldal的技术博客 (staldal.nu)
- Java开发人员在使用异常时犯的11个错误 (medium.com/@rafacdelnero)
- 检查异常是好是坏? (JavaWorld)
- 检查异常:Java的最大错误 精简Java (literatejava.com)
- 未经检查的异常-争议 (docs.oracle.com)
- 已检查异常的麻烦 (artima.com)
- Java例外:您(可能)做错了 (dzone.com)
- Java理论与实践:异常辩论– IBM (ibm.com)
- Java的检查异常是一个错误(这是我想做的 (radio-weblogs.com)
- Buggy Java代码:Java开发人员犯的十大最常见错误 Toptal (toptal.com)
您已经在Java 8上了吗? 生活变得好多了! 我… 呃…哦,等等。
- Java输入流的错误处理– Javamex (javamex.com)
- 在Java流中处理检查的异常 (oreilly.com)
- JDK 8流中的异常处理 (azul.com)
- 带有异常的Java 8功能接口 (slieb.org)
- 重新打包流中的异常– blog @ CodeFX (blog.codefx.org)
- 如何在Java 8 Stream中处理异常? –堆栈溢出 (stackoverflow.com)
- 检查异常和流| Benji的博客 (benjiweber.co.uk)
- 一个关于检查异常和Java 8 Lambda表达式的 故事 (javadevguy.wordpress.com)– 很好的战争故事!
- hgwood / java8-streams-and-exceptions (github.com)
- …
好的,看来您不可能正确地做到这一点。
至少,阅读上述名单后,我们现在完全取决于对速度的话题��
幸运的是,我不必再写一篇博客文章,介绍上面的文章已经涵盖了95%的内容,但是在这里,我将重点讨论代码中实际存在的一个Exception
��
副作用
既然您正在阅读这篇文章,您可能会对为什么这一切都与函数式编程有关感兴趣。
在以更“实用的方式”处理代码的过程中,您可能会遇到“副作用”一词,这是一件“坏事”。
在现实世界中, 副作用是您不希望发生的事情 ,您可能会说它等同于“例外”情况(您会例外说明),但是在函数式编程上下文中它具有更严格的含义。
维基百科有关副作用的文章说:
副作用(计算机科学)在计算机科学中,如果函数或表达式在返回范围之外修改了某些状态或与其调用函数或外界有可观察的交互作用,则称该函数或表达式具有副作用。 …在函数式编程中,很少使用副作用。
因此,在本系列的前两篇文章之后,让我们看看我们的FeedHandler代码当前的样子:
class FeedHandler {
Webservice webservice
DocumentDb documentDb
void handle(List<Doc> changes) {
changes
.findAll { doc -> isImportant(doc) }
.each { doc ->
try {
def resource = createResource(doc)
updateToProcessed(doc, resource)
} catch (e) {
updateToFailed(doc, e)
}
}
}
private Resource createResource(doc) {
webservice.create(doc)
}
private boolean isImportant(doc) {
doc.type == 'important'
}
private void updateToProcessed(doc, resource) {
doc.apiId = resource.id
doc.status = 'processed'
documentDb.update(doc)
}
private void updateToFailed(doc, e) {
doc.status = 'failed'
doc.error = e.message
documentDb.update(doc)
}
}
在一个地方,我们尝试捕获异常,在那儿,我们循环浏览重要的文档,并尝试为其创建“资源”(无论是什么)。
try {
def resource = createResource(doc)
updateToProcessed(doc, resource)
} catch (e) {
updateToFailed(doc, e)
}
在上面的代码中catch (e)
是catch (Exception e)
Groovy缩写。
是的,这就是我们正在捕获的通用java.lang.Exception
。 可以是任何例外,包括NPE。
如果createResource
方法没有引发异常,则将文档(“ doc”)更新为“已处理”,否则将其更新为“失败”。 顺便说一句,即使updateToProcessed
也会引发异常,但是对于当前的讨论,我实际上只对成功创建资源感兴趣。
因此,上面的代码可以工作 (我已经通过单元测试来证明它:-)),但是我对try-catch
语句不满意。 我只对成功创建资源感兴趣,而且很傻,我只能提出createResource
要么返回成功的资源, 要么抛出异常。
抛出异常以表示出了点问题,躲开闪避,让调用者捕获该异常以进行处理,这是为什么发明了异常的原因呢? 而且比返回null
更好吗?
它一直在发生。 采取一些我们喜欢的框架,例如JPA规范中的 EntityManager#find
:
啊! 返回null
。
返回值:
找到的实体实例;如果该实体不存在,则返回null
错误的例子。
函数式编程鼓励使用无副作用的方法(或:函数),以使代码更易于理解且更易于推理。 如果某个方法仅接受某些输入并每次都返回相同的输出(这使其成为一个纯函数),则各种优化都可以在后台进行,例如通过编译器或缓存,并行化等。
我们可以再次用纯函数(计算出的值)替换纯函数,这称为参考透明度 。
在上一篇文章中,我们已经将一些逻辑提取到了自己的方法中,例如下面的isImportant
。 给定相同的文档(具有相同的 type
属性)作为输入,我们每次都会获得相同的 (布尔值)输出。
boolean isImportant(doc) {
doc.type == 'important'
}
这里没有可观察到的副作用,没有全局变量被突变,没有日志文件被更新–它只是塞进,塞出 。
因此,我要说的是,通过我们的传统异常与外界交互的函数很少在函数式编程中使用。
我想做得更好 。 更好 。
可选救援
正如石磊韦伯表示它:
关于如何在Java中有效使用异常有不同的观点。 有些人喜欢检查异常,有些人则认为这是一次失败的实验,他们更喜欢独占使用未检查异常。 其他人则完全避开异常,而赞成传递和返回诸如Optional或Maybe之类的类型。
好的,让我们尝试一下Java 8的Optional
以便发出是否可以创建资源的信号。
让我们更改我们的webservice接口和createResource
方法,以在Optional
包装并返回我们的资源:
//private Resource createResource(doc) {
private Optional<Resource> createResource(doc) {
webservice.create(doc)
}
让我们更改原始的try-catch
:
try {
def resource = createResource(doc)
updateToProcessed(doc, resource)
} catch (e) {
updateToFailed(doc, e)
}
map
(处理资源)和orElseGet
(处理空的可选):
createResource(doc)
.map { resource ->
updateToProcessed(doc, resource)
}
.orElseGet { /* e -> */
updateToFailed(doc, e)
}
很棒的createResource
方法:返回正确结果,或者为空结果。
等一下! 唯一的例外e
我们需要传递到updateToFailed
是走了 :我们有一个空的Optional
替代。 我们不能存储的原因失败的原因 -这是我们做的必要性。
可能是Optional
只是表示“缺席”,并且是我们此处目的不正确的工具。
出色的完成
如果没有try-catch
和map-orElseGet
,我确实喜欢代码开始更多地反映操作“流程”的方式。 不幸的是,使用Optional
更适合“得到一些东西”或“什么也没有得到”(这也建议使用map
和orElseGet
类的名称),并且没有给我们提供记录失败原因的机会。
还有什么方法能够获得成功的结果或失败的原因,而仍然接近我们的阅读方式呢?
Future
。 更好的是: CompletableFuture
。
CompletableFuture
(CF)知道如何返回值,这类似于Optional
。 通常,CF用于将来获取值集 ,但这不是我们想要将其用于…的原因。
从Javadoc :
……支持……在完成时触发的行动的未来。
跳动,它可以表示“异常”完成 -给我机会对此采取行动。
让我们更改map
和orElseGet
:
createResource(doc)
.map { resource ->
updateToProcessed(doc, resource)
}
.orElseGet { /* e -> */
updateToFailed(doc, e)
}
thenAccept
(处理成功)并exceptionally
(处理失败):
createResource(doc)
.thenAccept { resource ->
updateToProcessed(doc, resource)
}
.exceptionally { e ->
updateToFailed(doc, e)
}
CompletableFuture#exceptionally
方法接受一个带有我们实际失败原因的异常e
的函数。
您可能会想: tomayto,tomahto。 首先,我们进行了try-catch
,现在我们进行了thenAccept-exceptionally
,那么有什么大不同?
好吧,我们显然不能摆脱特殊情况,但我们现在正在像Functionalville的居民那样思考:我们的方法开始成为函数 ,告诉我们有什么东西有事。
考虑到这是我们在第4部分中需要进行的少量重构,而在第5部分中,甚至更多地限制了代码中的副作用。
现在就这样
作为参考,这是重构代码的完整版本。
class FeedHandler {
Webservice webservice
DocumentDb documentDb
void handle(List<Doc> changes) {
changes
.findAll { doc -> isImportant(doc) }
.each { doc ->
createResource(doc)
.thenAccept { resource ->
updateToProcessed(doc, resource)
}
.exceptionally { e ->
updateToFailed(doc, e)
}
}
}
private CompletableFuture<Resource> createResource(doc) {
webservice.create(doc)
}
private boolean isImportant(doc) {
doc.type == 'important'
}
private void updateToProcessed(doc, resource) {
doc.apiId = resource.id
doc.status = 'processed'
documentDb.update(doc)
}
private void updateToFailed(doc, e) {
doc.status = 'failed'
doc.error = e.message
documentDb.update(doc)
}
}
-