JavaRush /Java 博客 /Random-ZH /本书的翻译。Java 中的函数式编程。第1章
timurnav
第 21 级

本书的翻译。Java 中的函数式编程。第1章

已在 Random-ZH 群组中发布
我很乐意帮助您发现错误并提高翻译质量。我翻译是为了提高我的英语语言技能,如果你阅读并查找翻译错误,那么你会比我进步得更好。该书的作者写道,这本书假设了很多使用 Java 的经验;说实话,我自己并不是特别有经验,但我理解了书中的内容。这本书涉及一些很难手动解释的理论。如果 wiki 上有不错的文章,我会提供它们的链接,但为了更好地理解,我建议您自己搜索 Google。祝大家好运。:) 对于那些想要修改我的翻译的人,以及那些觉得俄语读起来太差的人,你可以在这里下载原书。 目录 第 1 章你好,Lambda 表达式 - 目前正在阅读 第 2 章 使用集合 - 开发中 第 3 章 字符串、比较器和过滤器 - 开发中 第 4 章 使用 Lambda 表达式进行开发 - 开发中 第 5 章 使用资源 - 开发中 第 6 章 懒惰 - 开发中开发 第 7 章 优化资源 - 开发中 第 8 章 使用 lambda 表达式布局 - 开发中 第 9 章 整合 - 开发中

第 1 章 你好,Lambda 表达式!

我们的 Java 代码已准备好进行显着的转换。我们执行的日常任务变得更加简单、轻松且更具表现力。Java 编程的新方法已在其他语言中使用了数十年。通过对 Java 的这些更改,我们可以编写简洁、优雅、富有表现力且错误更少的代码。我们可以使用它来轻松应用标准并以更少的代码行实现常见的设计模式。在本书中,我们使用每天遇到的问题的简单示例来探索函数式编程风格。在我们深入研究这种优雅的风格和这种新的软件开发方式之前,让我们看看为什么它更好。
改变你的想法
命令式风格是 Java 自该语言诞生以来就赋予我们的风格。这种风格建议我们向 Java 描述我们希望该语言执行的每一步,然后我们只需确保忠实地遵循这些步骤即可。这很有效,但水平仍然很低。代码最终变得过于冗长,而我们经常想要一种更智能的语言。然后我们可以声明式地说出我们想要 什么,而不是深入研究 如何去做。感谢开发人员,Java 现在可以帮助我们做到这一点。让我们看几个示例来了解这些方法之间的优点和差异。
通常的方式
让我们从熟悉的基础知识开始,看看这两种范例的实际应用。这使用命令式方法在城市集合中搜索芝加哥 - 本书中的列表仅显示代码片段。 boolean found = false; for(String city : cities) { if(city.equals("Chicago")) { found = true; break; } } System.out.println("Found chicago?:" + found); 代码的命令式版本是嘈杂的(这个词与它有什么关系?)并且是低级的,有几个可变的部分。首先,我们创建这个名为 find 的臭布尔标志,然后迭代集合中的每个元素。如果我们找到了我们正在寻找的城市,我们将标志设置为 true并打破循环。最后我们将搜索结果打印到控制台。
有一个更好的方法
作为细心的 Java 程序员,浏览一下这段代码可以将其变成更具表现力和更易于阅读的内容,如下所示: System.out.println("Found chicago?:" + cities.contains("Chicago")); 这是声明式风格的示例 - contains() 方法可以帮助我们直接获得所需的内容。
实际变化
这些更改将为我们的代码带来相当大的改进:
  • 不用担心可变变量
  • 循环迭代隐藏在幕后
  • 减少代码混乱
  • 代码更加清晰,集中注意力
  • 阻抗较小;代码紧密跟踪业务意图
  • 出错的机会更少
  • 更容易理解和支持
超越简单的案例
这是一个声明性函数的简单示例,用于检查集合中是否存在元素;它已在 Java 中使用了很长时间。现在想象一下,不必为更高级的操作(例如解析文件、使用数据库、发出 Web 服务请求、创建多线程等)编写命令式代码。现在,Java 使得编写简洁、优雅的代码成为可能,从而使错误变得更加困难,不仅是在简单的操作中,而且在我们的整个应用程序中也是如此。
老办法
让我们看另一个例子。我们正在创建一个包含价格的集合,并将尝试多种方法来计算所有折扣价格的总和。 假设我们被要求汇总所有价值超过 20 美元的价格,并享受 10% 的折扣。让我们首先以通常的 Java 方式执行此操作。 这段代码对我们来说应该非常熟悉:首先我们创建一个可变变量 totalOfDiscountedPrices,我们将在其中存储结果值。然后,我们循环访问价格集合,选择高于 20 美元的价格,获取折扣价,并将该值添加到 totalOfDiscountedPrices中。最后,我们显示考虑折扣后所有价格的总和。以下是控制台的输出内容 final List prices = Arrays.asList( new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"), new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"), new BigDecimal("45"), new BigDecimal("12")); BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO; for(BigDecimal price : prices) { if(price.compareTo(BigDecimal.valueOf(20)) > 0) totalOfDiscountedPrices = totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9))); } System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
折扣总价:67.5
可以用,但是代码看起来很乱。但这不是我们的错,我们使用了可用的东西。该代码的级别相当低 - 它受到对原语的痴迷(谷歌它,有趣的东西)并且它违背了 单一责任原则。我们这些在家工作的人应该让这样的代码远离那些立志成为程序员的孩子们的眼睛,这可能会惊动他们脆弱的心灵,准备好面对“这是你为了生存而必须做的事情吗?”的问题。
有一个更好的方法,另一种
现在我们可以做得更好,更好。我们的代码可能类似于规范要求。这将帮助我们缩小业务需求和实现它们的代码之间的差距,进一步减少需求被误解的可能性。让我们在更高的抽象级别上工作,而不是创建一个变量然后重复更改它,例如下面的清单。 final BigDecimal totalOfDiscountedPrices = prices.stream() .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9))) .reduce(BigDecimal.ZERO, BigDecimal::add); System.out.println("Total of discounted prices: " + totalOfDiscountedPrices); 让我们大声读出来 - 价格过滤器大于 20,使用“价格”键进行映射(创建“键”“值”对),价格包括折扣,然后将它们相加
- 译者的注释是指在阅读代码时出现在您脑海中的单词 .filter(price ->price.compareTo(BigDecimal.valueOf(20)) > 0)
代码按照我们所读到的相同逻辑顺序一起执行。代码被缩短了,但我们使用了 Java 8 中的一大堆新东西。首先,我们在 价格列表上调用了stream()方法。这为自定义迭代器打开了大门,该迭代器具有丰富的便利功能,我们将在稍后讨论。 我们没有直接循环价格列表中的所有值,而是使用几种特殊的方法,例如 filter()map()。与我们在 Java 和 JDK 中使用的方法不同,这些方法采用匿名函数(lambda 表达式)作为括号中的参数。我们稍后会更详细地研究它。 通过调用reduce()方法,我们计算 map()方法中获得的值(折扣价)的总和。 循环的隐藏方式与使用contains()方法时的方式相同。 然而, filter()map()方法更加复杂。 对于价格列表中的每个价格,他们调用传递的 lambda 函数并将其保存到新集合中。对此集合调用 reduce()方法以产生最终结果。以下是控制台的输出内容
折扣总价:67.5
变化
以下是相对于通常方法的变化:
  • 代码赏心悦目,不杂乱。
  • 没有低级操作
  • 更容易改进或改变逻辑
  • 迭代由方法库控制
  • 高效、惰性循环评估
  • 根据需要更容易并行化
稍后我们将讨论 Java 如何提供这些改进。
兰姆达来救援:)
Lambda 是将我们从命令式编程的麻烦中解放出来的功能关键。通过改变我们编程的方式,借助Java的最新特性,我们可以写出的代码不仅优雅简洁,而且不易出错,效率更高,更容易优化、改进、制作多线程。
从函数式编程中获益匪浅
函数式编程风格具有更高的 信噪比;我们编写的代码行更少,但每行或表达式执行更多功能。与命令式相比,我们从函数式代码版本中获得的收益很少:
  • 我们避免了不必要的变量更改或重新分配,这些更改或重新分配是错误的来源,并且使得同时处理来自不同线程的代码变得困难。在命令式版本中,我们在整个循环中为totalOfDiscountedPrices变量设置不同的值。在函数式版本中,代码中的变量没有显式更改。更少的更改会导致代码中的错误更少。
  • 代码的函数版本更容易并行化。即使map()方法中的计算很长,我们也可以并行运行它们而不必担心。如果我们从不同的线程访问命令式代码,我们将需要担心同时更改totalOfDiscountedPrices变量。在函数式版本中,我们只有在完成所有更改后才访问变量,这使我们不必担心代码的线程安全性。
  • 代码更具表现力。我们不需要分几个步骤执行代码 - 创建并用虚拟值初始化变量、循环访问价格列表、向变量添加折扣价格等等 - 我们只需要求列表的 map() 方法返回另一个列表折扣价格并将它们相加。
  • 函数式风格更加简洁:比命令式版本需要更少的代码行。更紧凑的代码意味着更少的编写、阅读和维护。
  • 一旦您了解其语法,该代码的函数版本就非常直观且易于理解。map()方法将传递的函数(计算折扣价格)应用于集合的每个元素,并生成包含结果的集合,如下图所示。

图1-map方法将传递的函数应用到集合的每个元素
在 lambda 表达式的支持下,我们可以充分利用 Java 函数式编程的威力。如果我们掌握了这种风格,我们就可以创建更具表现力、更简洁的代码,并且更改和错误更少。以前,Java 的主要优势之一是它对面向对象范例的支持。而且函数式风格并不与OOP相矛盾。从命令式编程转向声明式编程的真正卓越。借助 Java 8,我们可以非常有效地将函数式编程与面向对象风格结合起来。我们可以继续将 OO 风格应用于对象、它们的范围、状态和关系。此外,我们还可以将行为和状态变化、业务流程和数据处理建模为一系列功能集。
为什么要以函数式风格编写代码?
我们已经看到了函数式编程风格的整体优势,但是这种新风格值得学习吗?这只是语言上的微小变化还是会改变我们的生活?在浪费时间和精力之前,我们必须找到这些问题的答案。编写 Java 代码并不困难;该语言的语法很简单。我们对熟悉的库和 API 感到满意。真正需要我们花精力编写和维护代码的是我们使用Java进行开发的典型企业应用程序。我们需要确保其他程序员在正确的时间关闭与数据库的连接,确保他们不会持有数据库或执行事务的时间超过必要的时间,确保他们在正确的级别完全捕获异常,确保他们正确地应用和释放锁。 ...这张纸可以持续很长时间。上述每一个论点单独来看都没有什么分量,但是当与固有的实现复杂性相结合时,它就变得势不可挡、耗时且难以实现。如果我们可以将这些复杂性封装成能够很好地管理它们的小代码片段,会怎么样?那么我们就不会不断地花费精力去执行标准。这将带来很大的优势,所以让我们看看函数式风格如何提供帮助。
乔问
短*代码是否仅仅意味着更少的代码字母?
* 我们谈论的是“简洁”这个词,它描述了使用 lambda 表达式的代码的函数式风格
在这种情况下,代码应该简洁,没有多余的装饰,并减少直接影响,以更有效地传达意图。这些都是深远的好处。编写代码就像将配料放在一起:使其简洁就像为其添加酱汁。有时编写这样的代码需要花费更多的精力。需要阅读的代码较少,但它使代码更加透明。缩短代码时保持代码清晰非常重要。简洁的代码类似于设计技巧。此代码需要较少的手鼓跳舞。这意味着我们可以快速实施我们的想法,如果它们有效,我们可以继续前进,如果它们不符合期望,我们可以放弃它们。
类固醇的迭代
我们使用迭代器来处理对象列表,以及使用集合和映射。我们在 Java 中使用的迭代器是我们所熟悉的;虽然它们很原始,但并不简单。它们不仅占用大量代码,而且编写起来也相当困难。我们如何迭代集合的所有元素?我们可以使用 for 循环。我们如何从集合中选择一些元素?使用相同的 for 循环,但使用一些额外的可变变量,这些变量需要与集合中的某些内容进行比较。那么,选择特定值后,我们如何对单个值进行操作,例如最小值、最大值或某个平均值?再次循环,再次出现新的变量。这让人想起谚语“只见树木,不见森林”(原文使用了与迭代相关的双关语,意思是“一切都已完成,但并非一切都成功”——译者注)。jdk 现在为各种语句提供内部迭代器:一个用于简化循环,一个用于绑定所需的结果依赖项,一个用于过滤输出值,一个用于返回值,以及几个用于获取最小值、最大值、平均值等的便利函数。此外,这些操作的功能可以非常容易地组合,这样我们就可以组合不同的操作集,以更轻松、更少的代码来实现业务逻辑。完成后,代码将更容易理解,因为它按照问题所需的顺序创建逻辑解决方案。我们将在本书的第 2 章和后面的部分中查看此类代码的一些示例。
算法应用
算法驱动企业应用程序。比如我们需要提供一个需要权限检查的操作。我们必须确保交易快速完成并正确完成检查。此类任务通常会简化为非常普通的方法,如下面的清单所示: Transaction transaction = getFromTransactionFactory(); //... Операция выполняющаяся во время транзакции... checkProgressAndCommitOrRollbackTransaction(); UpdateAuditTrail(); 这种方法有两个问题。首先,这通常会导致开发工作量加倍,进而导致维护应用程序的成本增加。其次,很容易错过该应用程序代码中可能抛出的异常,从而危及交易的执行和检查的通过。我们可以使用适当的 try-finally 块,但是每次有人触及这段代码时,我们都需要重新检查代码的逻辑是否没有被破坏。否则,我们可以放弃工厂并彻底改变整个代码。我们可以将处理代码发送到管理良好的函数,而不是接收交易,例如下面的代码。 runWithinTransaction((Transaction transaction) -> { //... Операция выполняющаяся во время транзакции... }); 这些小的更改加起来可以节省大量成本。状态检查和应用程序检查的算法被赋予了新的抽象级别,并使用 runWithinTransaction()方法进行封装。在此方法中,我们放置了一段应在事务上下文中执行的代码。我们不再需要担心忘记做某事或者是否在正确的位置捕获了异常。算法函数可以解决这个问题。这个问题将在第 5 章中更详细地讨论。
算法扩展
算法的使用越来越频繁,但为了让它们充分用于企业应用程序开发,需要扩展它们的方法。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION