Java函数式编程(一):你好,Lambda表达式

前端技术 2023/09/01 Java

第一章 你好,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); 

这个命令式的版本看起来有点啰嗦而且初级;它分成好几个执行部分。先是初始化一个叫found的布尔标记,然后遍历集合里的每一个元素;如果发现我们要找的城市了,设置下这个标记,然后跳出循环体;最后打印出查找的结果。

一种更好的方式

细心的Java程序员看完这段代码后,很快会想到一种更简洁明了的方式,就像这样:

复制代码 代码如下:

System.out.println(\"Found chicago?:\" + cities.contains(\"Chicago\")); 

这也是一种命令式风格的写法——contains方法直接就帮我们搞定了。

实际改进的地方

代码这么写有这几个好处:

1.不用再捣鼓那个可变的变量了
2.将迭代封装到了底层
3.代码更简洁
4.代码更清晰,更聚焦
5.少走弯路,代码和业务需求结合更密切
6.不易出错
7.易于理解和维护

来个复杂点的例子

这个例子太简单了,命令式查询一个元素是否存在于某个集合在Java里随处可见。现在假设我们要用命令式编程来进行些更高级的操作,比如解析文件 ,和数据库交互,调用WEB服务,并发编程等等。现在我们用Java可以写出更简洁优雅同时出错更少的代码,更不只是这种简单的场景。

老的方式

我们来看下另一个例子。我们定义了一系列价格,并通过不同的方式来计算打折后的总价。

复制代码 代码如下:

final List<BigDecimal> 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\")); 

假设超过20块的话要打九折,我们先用普通的方式实现一遍。

复制代码 代码如下:

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

这个代码很熟悉吧;先用一个变量来存储总价;然后遍历所有的价格,找出大于20块的,算出它们的折扣价,并加到总价里面;最后打印出折扣后的总价。

下面是程序的输出:

复制代码 代码如下:

Total of discounted prices: 67.5 

结果完全正确,不过这样的代码有点乱。这并不是我们的错,我们只能用已有的方式来写。不过这样的代码实在有点初级,它不仅存在基本类型偏执,而且还违反了单一职责原则。如果你是在家工作并且家里还有想当码农的小孩的话,你可得把你的代码藏好了,万一他们看见了会很失望地叹气道,“你是靠这些玩意儿糊口的?”

还有更好的方式

我们还能做的更好——并且要好很多。我们的代码有点像需求规范。这样能缩小业务需求和实现的代码之间的差距,减少了需求被误读的可能性。

我们不再让Java去创建一个变量然后没完没了的给它赋值了,我们要从一个更高层次的抽象去与它沟通,就像下面的这段代码。

复制代码 代码如下:

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块的价格,把它们转化成折扣价,然后加起来。这段代码和我们描述需求的流程简直一模一样。Java里还可以很方便的把一行长的代码折叠起来,根据方法名前面的点号进行按行对齐,就像上面那样。

代码非常简洁,不过我们用到了Java8里面的很多新东西。首先,我们调用 了价格列表的一个stream方法。这打开了一扇大门,门后边有数不尽的便捷的迭代器,这个我们在后面会继续讨论。

我们用了一些特殊的方法,比如filter和map,而不是直接的遍历整个列表。这些方法不像我们以前用的JDK里面的那些,它们接受一个匿名的函数——lambda表达式——作为参数。(后面我们会深入的展开讨论)。我们调用reduce()方法来计算map()方法返回的价格的总和。

就像contains方法那样,循环体被隐藏起来了。不过map方法(以及filter方法)则更复杂得多 。它对价格列表中的每一个价格,调用了传进来的lambda表达式进行计算,把结果放到一个新的集合里面。最后我们在这个新的集合上调用 reduce方法得出最终的结果。

这是以上代码的输出结果:

复制代码 代码如下:

Total of discounted prices: 67.5 

改进的地方

这和前面的实现相比改进明显:

1.结构良好而不混乱
2.没有低级操作
3.易于增强或者修改逻辑
4.由方法库来进行迭代
5.高效;循环体惰性求值
6.易于并行化

下面我们会说到Java是如何实现这些的。

lambda表达式来拯救世界了

lambda表达式是让我们远离命令式编程烦恼的快捷键。Java提供的这个新特性,改变了我们原有的编程方式,使得我们写出的代码不仅简洁优雅,不易出错,而且效率更高,易于优化改进和并行化。

第二节:函数式编程的最大收获

函数式风格的代码有更高的信噪比;写的代码更少了,但每一行或者每个表达式做的却更多了。比命令式编程相比,函数式编程让我们获益良多:

避免了对变量的显式的修改或赋值,这些通常是BUG的根源,并导致代码很难并行化。在命令行编程中我们在循环体内不停的对totalOfDiscountedPrices变量赋值。在函数式风格里,代码不再出现显式的修改操作。变量修改的越少,代码的BUG就越少。

函数式风格的代码可以轻松的实现并行化。如果计算很费时,我们可以很容易让列表中的元素并发的执行。如果我们想把命令式的代码并行化,我们还得担心并发修改totalOfDiscountedPrices变量带来的问题。在函数式编程中我们只会在完全处理完后才访问这个变量,这样就消除了线程安全的隐患。

代码的表达性更强。命令式编程要分成好几个步骤要说明要做什么——创建一个初始化的值,遍历价格,把折扣价加到变量上等等——而函数式的话只需要让列表的map方法返回一个包括折扣价的新的列表然后进行累加就可以了。
函数式编程更简洁;和命令式相比同样的结果只需要更少的代码就能完成。代码更简洁意味着写的代码少了,读的也少了,维护的也少了——看下第7页的\"简洁少就是简洁了吗\"。

函数式的代码更直观——读代码就像描述问题一样——一旦我们熟悉语法后就很容易能看懂。map方法对集合的每个元素都执行了一遍给定的函数(计算折扣价),然后返回结果集,就像下图演示的这样。


图1——map对集合中的每个元素执行给定的函数

有了lambda表达式之后,我们可以在Java里充分发挥函数式编程的威力。使用函数式风格,就能写出表达性更佳,更简洁,赋值操作更少,错误更少的代码了。
支持面向对象编程是Java一个主要的优点。函数式编程和面向对象编程并不排斥。真正的风格变化是从命令行编程转到声明式编程。在Java 8里,函数式和面向对象可以有效的融合到一起。我们可以继续用OOP的风格来对领域实体以及它们的状态,关系进行建模。除此之外,我们还可以对行为或者状态的转变,工作流和数据处理用函数来进行建模,建立复合函数。

第三节:为什么要用函数式风格?

我们看到了函数式编程的各项优点,不过使用这种新的风格划得来吗?这只是个小改进还是说换头换面?在真正在这上面花费工夫前,还有很多现实的问题需要解答。

复制代码 代码如下:

小明问到:
代码少就是简洁了吗?

简洁是少而不乱,归根结底是说要能有效的表达意图。它带来的好处意义深远。
写代码就好像把配料堆到一起,简洁就是说能把配料调成调料。要写出简洁的代码可得下得狠工夫。读的代码是少了,真正有用的代码对你是透明的。一段很难理解或者隐藏细节的短代码只能说是简短而不是简洁。

简洁的代码竟味着敏捷的设计。简洁的代码少了那些繁文缛节。这是说我们可以对想法进行快速尝试,如果不错就继续,如果效果不佳就迅速跳过。

本文地址:https://www.stayed.cn/item/2917

转载请注明出处。

本站部分内容来源于网络,如侵犯到您的权益,请 联系我

我的博客

人生若只如初见,何事秋风悲画扇。