Java从函数式编程中引入的两个核心思想:将方法和Lambda
作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。前面说到的新的Stream API
把这两种思想都用到了。
常见的函数式语言,如SML
、OCaml
、Haskell
,还提供了进一步的结构来帮助程序员。其中之一就是通过使用更多的描述性数据类型来避免null
。
在Java 8
里有一个optional
类,如果能一致地使用它的话,就可以帮助避免出现NullPointer
异常。它是一个容器对象,可以包含,也可以不包含一个值。Optional
中有方法来明确处理值不存在的情况,这样就可以避免NullPointer
异常了。换句话说,它使用类型系统,允许表明一个变量可能会没有值。
第二个想法是(结构)模式匹配。这在数学中也有使用,例如:
f(0) = 1
f(n)= n * f(n - 1) otherwise
在Java中,可以在这里写一个if-then-else
语句或一个switch
语句。其他语言表明,对于更复杂的数据类型,模式匹配可以比if-then-else
更简明地表达编程思想。对于这种数据类型,也可以使用多态和方法重载来替代if-then-else
,但对于哪种方式更合适,就语言设计而言仍有一些争论。
Lambda
是怎样写的。Java 8
中Streams
的概念使得Collections
的许多方面得以推广,让代码更为易读,并允许并行处理流元素。null
和使用模式匹配。在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:"其实我还想找出所有重量超过150克的苹果。"又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”要如何应对这样不断变化的需求?理想的状态下,应该把工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
行为参数化就是可以帮助处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被程序的其他部分调用,这意味着可以推迟这块代码的执行。例如,可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果要处理一个集合,可能会写一个方法:
行为参数化说的就是这个。打个比方:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy
方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go
,它可以接受不同的新行为作为参数,然后去执行。
几个真实的例子。比如,行为参数化模式——使用JavaAPI
中现有的类和接口,对List
进行排序,筛选文件名,或告诉一个Thread
去执行代码块,甚或是处理GUI
事件。很快会发现,在Java
中使用这种模式十分啰嗦。Java 8
中的Lambda
解决了代码啰嗦的问题。
第一个解决方案可能是下面这样的:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<Apple>(); //累积苹果的列表
for(Apple apple: inventory) {
if("green".equals(apple.getcolor()) { //仅仅选出绿苹果
result.add(apple);
}
}
return result;
}
突出显示的行就是筛选绿苹果所需的条件。但是现在农民改主意了,他还想要筛选红苹果。该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples
,然后更改if
条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory) {
if ( apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
现在,只要像下面这样调用方法,农民朋友就会满意了:
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
让我们把例子再弄得复杂一点儿。这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:
public static List<Apple> filterApplesByWeight (List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<Apple>();
for(Apple apple: inventory) {
if(apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则。如果想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。
可以将颜色和重量结合为一个方法,称为filter
。不过就算这样,还是需要一种方式来区分想要筛选哪个属性。可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!)。
一种把所有属性结合起来的笨拙尝试如下所示:
public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for(Apple apple : inventory) {
//十分笨拙的选择颜色或重量的方式
if((flag apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}
你可以这么用(但真的很笨拙):
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
这个解决方案再差不过了。首先,客户端代码看上去糟透了。true
和false
是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?会有好多个重复的filter
方法,或一个巨大的非常复杂的方法。到目前为止,已经给filterApples
方法加上了值(比如string
、Integer
或boolean
)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,需要一种更好的方式,来把苹果的选择标准告诉filterApples
方法。