函数式 Java 实例 |第 3 部分——不要使用Exception来控制流程

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(105)   2024-06-16 16:20:26

这是名为“Functional Java by Example”系列的第 3 部分。

我在本系列的每个部分中发展的示例是某种处理文档的“提要处理程序”。在前面的部分中,我从一些原始代码开始,并应用一些重构来描述“什么”而不是“如何”。

为了帮助代码向前发展,我们需要摆脱 good ol' java.lang.Exception。 (免责声明:我们实际上无法摆脱它)这就是这部分的用武之地。

如果您是第一次来这里,最好从头开始阅读。它有助于了解我们从哪里开始以及我们如何在整个系列中前进。

这些是所有部分:

  • 第 1 部分 – 从命令式到声明式
  • 第 2 部分 – 讲故事
  • 第 3 部分 – 不要使用异常来控制流程
  • 第 4 部分 – 更喜欢不变性
  • 第 5 部分 – 将 I/O 移至外部
  • 第 6 部分 - 作为参数的函数
  • 第 7 部分 – 也将失败视为数据
  • 第 8 部分 – 更多纯函数

我将在每篇文章发表时更新链接。如果您通过内容联合阅读本文,请查看我的博客 上的原始文章。

每次也将代码推送到此 GitHub 项目

快速了解异常

我们的 java.lang.Exception 从 Java 1.0 开始就存在了——基本上是我们在顺境时的朋友和其他时候的宿敌。

关于它们没什么可说的,但如果你想阅读一些资料,这里是我最喜欢的:

  • Java 中的异常 (JavaWorld)
  • Java 中的异常 – GeeksforGeeks (geeksforgeeks.org)
  • 在 Java 中处理异常的 9 个最佳实践 (stackify.com)
  • 异常处理的最佳实践 (onjava.com)
  • Java 异常面试问题和答案 (journaldev.com)
  • Java 中的异常处理示例 (beginnersbook.com)
  • Java 异常处理(Try-catch) (hackerrank.com)
  • 20 大 Java 异常处理最佳实践 – HowToDoInJava (howtodoinjava.com)
  • Java 中的异常处理和断言 – NTU (ntu.edu.sg)
  • 异常处理:最佳实践指南 (dzone.com)
  • 在 Java 中处理异常的 9 个最佳实践 (dzone.com)
  • 修复 7 个常见的 Java 异常处理错误 (dzone.com)
  • Java 实践 -> 检查异常与未检查异常 (javapractices.com)
  • Java 中异常的常见错误 | Mikael Stå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)
  • 错误的 Java 代码:Java 开发人员最常犯的 10 大错误 | Toptal (toptal.com)

你已经在使用 Java 8 了吗?生活变得如此美好!我……呃……哦,等等。

  • 使用 Java 输入流进行错误处理 – Javamex (javamex.com)
  • 处理 Java 流中的检查异常 (oreilly.com)
  • JDK 8 Streams 中的异常处理 (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 &# 55357;�

副作用

既然您正在阅读这篇文章,您可能会对为什么这一切都与函数式编程有关感兴趣。

在以更“函数式”的方式处理代码的过程中,您可能遇到过“副作用”一词,认为这是一件“坏事”。

在现实世界中,副作用是您不希望发生的事情,您可能会说它等同于“异常”情况(您会用例外来表示),但它有更多在函数式编程上下文中的严格含义。

关于 Side effect 的维基百科文章说:

副作用(计算机科学)在计算机科学中,如果一个函数或表达式修改了其范围之外的某些状态,或者除了返回值之外与其调用函数或外部世界具有可观察到的交互,则称该函数或表达式具有副作用。 …在函数式编程中,很少使用副作用。

因此,在本系列的前两篇文章之后,让我们看看我们的 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-catch 异常,那是我们遍历重要文档 并尝试为其创建“资源”(无论是什么)的地方。

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”)更新为“processed”,否则我们将其更新为“failed”。 顺便说一句,即使 updateToProcessed 也可以抛出异常,但对于当前的讨论,我实际上只对成功的资源创建感兴趣。

所以,上面的代码有效(我有单元测试来证明它:-))但是我对现在的try-catch 语句不满意。我只对成功的资源创建感兴趣,愚蠢的我,我只能想出 createResource 要么返回成功的资源 要么 抛出异常。

抛出异常以表明出现问题,躲避,让调用者捕获异常以处理它,这就是异常被发明的原因吗?这比返回 null 更好,对吧?

它一直在发生。以我们最喜欢的一些框架为例,例如 JPA 规范 中的 EntityManager#find

精!返回 null

返回:找到的实体实例,如果实体不存在则返回null

错误的例子。

函数式编程鼓励无副作用的方法(或:函数),以使代码更易于理解和推理。如果一个方法每次只接受特定的输入并返回相同的输出——这使它成为一个函数——所有类型的优化都可以在幕后发生,例如由编译器,或缓存,并行化等。

我们可以用它们的(计算的)值再次替换函数,这被称为referential transparancy

在上一篇文章中,我们已经将一些逻辑提取到它们自己的方法中,例如下面的isImportant。给定 same 文档(具有 same type 属性)作为输入,我们将得到 same (布尔值) 每次输出。

boolean isImportant(doc) {
  doc.type == 'important'
}

这里没有可观察到的副作用,没有全局变量发生变化,没有日志文件更新——它只是填充,填充

因此,我想说的是,通过传统异常与外界交互的函数很少用于函数式编程。

我想做得比那更好变得更好。

可选的救援

正如 Benji Weber 表达的那样:

关于如何在 Java 中有效地使用异常存在不同的观点。有些人喜欢受检异常,有些人认为这是一个失败的实验,并且更喜欢独占使用未受检异常。其他人则完全避开异常,转而支持传递和返回类型,例如 Optional 或 Maybe。

好的,让我们试试 Java 8 的 Optional 来指示资源是否可以创建。

让我们更改我们的网络服务接口和 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 更适合“得到一些东西”或“什么也得不到”(也建议使用 maporElseGet 这样的名称)并且没有让我们有机会记录失败的原因。

获得成功结果或获得失败原因的另一种方法是什么,仍然接近我们的阅读方式?

一个Future。更好的是:CompletableFuture

CompletableFuture (CF) 知道如何返回一个值,在这方面它类似于 Optional。通常 CF 用于获取将来设置的值,但这不是我们想要使用它的目的……

来自 Javadoc

一个 Future ……,支持……在其完成时触发的动作。

吉普,它可以发出“异常”完成的信号——让我有机会根据它采取行动。

让我们更改 maporElseGet

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

}

标签2: Java教程
地址:https://www.cundage.com/article/jcg-functional-java-example-part-3-dont-use-exceptions-control-flow.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...