Java函数式编程(一)

Published: 12 Mar 2014 Category: Java-functional-programming

本系列文章译自Venkat Subramaniam的Functional Programming in 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方法直接就帮我们搞定了。

实际改进的地方

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

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

来个复杂点的例子

这个例子太简单了,命令式查询一个元素是否存在于某个集合在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

改进的地方

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

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

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

lambda表达式来拯救世界了

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

未完待续,后续文章请继续关注deepinmind

原创文章转载请注明出处:Java函数式编程(一)