本章内容
应对不断变化的需求
行为参数化
匿名类
Lambda表达式预览
真实示例:
Comparator
、Runnable
和GUI
在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果你要处理一个集合,可能会写一个方法:
可以对列表中的每个元素做“某件事”
可以在列表处理完后做“另一件事”
遇到错误时可以做“另外一件事”
行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy
方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go
,它可以接受不同的新行为作为参数,然后去执行。
这一章首先会给你讲解一个例子,说明如何对你的代码加以改进,从而更灵活地适应不断变化的需求。在此基础之上,我们将展示如何把行为参数化用在几个真实的例子上。比如,你可能已经用过了行为参数化模式——使用Java API中现有的类和接口,对List
进行排序,筛选文件名,或告诉一个Thread
去执行代码块,甚或是处理GUI事件。你很快会发现,在Java中使用这种模式十分啰嗦。Java 8中的Lambda解决了代码啰嗦的问题。我们会在第3章中向你展示如何构建Lambda表达式、其使用场合,以及如何利用它让代码更简洁。
2.1 应对不断变化的需求
编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。听起来很简单吧?
2.1.1 初试牛刀:筛选绿苹果
第一个解决方案可能是下面这样的:
public static List<Apple> filterGreenApples(List<Apple> inventory) { List<Apple> result = new ArrayList<>; ←─累积苹果的列表 for(Apple apple: inventory){ if( "green".equals(apple.getColor ) { ←─仅仅选出绿苹果 result.add(apple); } } return result;}
突出显示的行就是筛选绿苹果所需的条件。但是现在农民改主意了,他还想要筛选红苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples
,然后更改if
条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
2.1.2 再展身手:把颜色作为参数
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) { List<Apple> result = new ArrayList<>; 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<>; For (Apple apple: inventory){ if ( apple.getWeight > weight ){ result.add(apple); } } return result;}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了DRY(Don't Repeat Yourself,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。
你可以将颜色和重量结合为一个方法,称为filter
。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!我们很快会解释为什么)。
2.1.3 第三次尝试:对你能想到的每个属性做筛选
一种把所有属性结合起来的笨拙尝试如下所示:
public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) { List<Apple> result = new ArrayList<>; 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
方法。在下一节中,我们会介绍了如何利用行为参数化实现这种灵活性。
2.2 行为参数化
你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple
的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean
值。我们把它称为谓词(即一个返回boolean
值的函数)。让我们定义一个接口来对选择标准建模:
public interface ApplePredicate{ boolean test (Apple apple);}
现在你就可以用ApplePredicate
的多个实现代表不同的选择标准了,比如(如图2-1所示):
public class AppleHeavyWeightPredicate implements ApplePredicate{ ←─仅仅选出重的苹果 public boolean test(Apple apple){ return apple.getWeight > 150; }}public class AppleGreenColorPredicate implements ApplePredicate{ ←─仅仅选出绿苹果 public boolean test(Apple apple){ return "green".equals(apple.getColor); }}
图 2-1 选择苹果的不同策略
你可以把这些标准看作filter
方法的不同行为。你刚做的这些和“策略设计模式”1相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate
,不同的策略就是AppleHeavyWeightPredicate
和AppleGreenColorPredicate
。
1见http://en.wikipedia.org/wiki/Strategy_pattern。
但是,该怎么利用ApplePredicate
的不同实现呢?你需要filterApples
方法接受ApplePredicate
对象,对Apple
做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
要在我们的例子中实现这一点,你要给filterApples
方法添加一个参数,让它接受ApplePredicate
对象。这在软件工程上有很大好处:现在你把filterApples
方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
第四次尝试:根据抽象条件筛选
利用ApplePredicate
改过之后,filter
方法看起来是这样的:
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){ List<Apple> result = new ArrayList<>; for(Apple apple: inventory){ if(p.test(apple)){ ←─谓词对象封装了测试苹果的条件 result.add(apple); } } return result;}
1. 传递代码/行为
这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate
对象,并将它们传递给filterApples
方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate
就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:
public class AppleRedAndHeavyPredicate implements ApplePredicate{ public boolean test(Apple apple){ return "red".equals(apple.getColor) && apple.getWeight > 150; }}List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate);
你已经做成了一件很酷的事:filterApples
方法的行为取决于你通过ApplePredicate
对象传递的代码。换句话说,你把filterApples
方法的行为参数化了!
请注意,在上一个例子中,唯一重要的代码是test
方法的实现,如图2-2所示;正是它定义了filterApples
方法的新行为。但令人遗憾的是,由于该filterApples
方法只能接受对象,所以你必须把代码包裹在ApplePredicate
对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test
方法的对象来传递布尔表达式的。你将在2.3节(第3章中有更详细的内容)中看到,通过使用Lambda,你可以直接把表达式"red".equals(apple.getColor) &&apple.getWeight > 150
传递给filterApples
方法,而无需定义多个ApplePredicate
类,从而去掉不必要的代码。
图 2-2 参数化filterApples
的行为,并传递不同的筛选策略
2. 多种行为,一个参数
正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如图2-3所示。
图 2-3 参数化filterApples
的行为并传递不同的筛选策略
这就是说行为参数化 是一个有用的概念的原因。你应该把它放进你的工具箱里,用来编写灵活的API。
为了保证你对行为参数化运用自如,看看测验2.1吧!
测验2.1:编写灵活的
prettyPrintApple
方法编写一个
prettyPrintApple
方法,它接受一个Apple
的List
,并可以对它参数化,以多种方式根据苹果生成一个String
输出(有点儿像多个可定制的toString
方法)。例如,你可以告诉prettyPrintApple
方法,只打印每个苹果的重量。此外,你可以让prettyPrintApple
方法分别打印每个苹果,然后说明它是重的还是轻的。解决方案和我们前面讨论的筛选的例子类似。为了帮你上手,我们提供了prettyPrintApple
方法的一个粗略的框架:public static void prettyPrintApple(List<Apple> inventory, ???){ for(Apple apple: inventory) { String output = ???.???(apple); System.out.println(output); }}
答案如下。
首先,你需要一种表示接受
Apple
并返回一个格式String
值的方法。前面我们在编写ApplePredicate
接口的时候,写过类似的东西:public interface AppleFormatter{ String accept(Apple a);}
现在你就可以通过实现
AppleFormatter
方法,来表示多种格式行为了:public class AppleFancyFormatter implements AppleFormatter{ public String accept(Apple apple){ String characteristic = apple.getWeight > 150 ? "heavy" : "light"; return "A " + characteristic + " " + apple.getColor +" apple"; }}public class AppleSimpleFormatter implements AppleFormatter{ public String accept(Apple apple){ return "An apple of " + apple.getWeight + "g"; }}
最后,你需要告诉
prettyPrintApple
方法接受AppleFormatter
对象,并在内部使用它们。你可以给prettyPrintApple
加上一个参数:public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){ for(Apple apple: inventory){ String output = formatter.accept(apple); System.out.println(output); }}
搞定啦!现在你就可以给
prettyPrintApple
方法传递多种行为了。为此,你首先要实例化AppleFormatter
的实现,然后把它们作为参数传给prettyPrintApple
:prettyPrintApple(inventory, new AppleFancyFormatter);
这将产生一个类似于下面的输出:
A light green appleA heavy red apple…
或者试试这个:
prettyPrintApple(inventory, new AppleSimpleFormatter);
这将产生一个类似于下面的输出:
An apple of 80gAn apple of 155g…
你已经看到,可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。让我们来看看可以怎样改进。
2.3 对付啰嗦
我们都知道,人们都不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给filterApples
方法的时候,你不得不声明好几个实现ApplePredicate
接口的类,然后实例化好几个只会提到一次的ApplePredicate
对象。下面的程序总结了你目前看到的一切。这真是很啰嗦,很费时间!
代码清单2-1 行为参数化:用谓词筛选苹果
public class AppleHeavyWeightPredicate implements ApplePredicate{ ←─选择较重苹果的谓词 public boolean test(Apple apple){ return apple.getWeight > 150; }}public class AppleGreenColorPredicate implements ApplePredicate{ ←─选择绿苹果的谓词 public boolean test(Apple apple){ return "green".equals(apple.getColor); }}public class FilteringApples{ public static void main(String...args){ List<Apple> inventory = Arrays.asList(new Apple(80,"green"), new Apple(155, "green"), new Apple(120, "red")); List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate); ←─结果是一个包含一个155克Apple的List List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate); ←─结果是一个包含两个绿Apple的List } public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) { List<Apple> result = new ArrayList<>; for (Apple apple : inventory){ if (p.test(apple)){ result.add(apple); } } return result; }}
费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。但这也不完全令人满意。2.3.3节简短地介绍了Lambda表达式如何让你的代码更易读,我们将在下一章详细讨论。
2.3.1 匿名类
匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。
2.3.2 第五次尝试:使用匿名类
下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate
的对象,重写筛选的例子:
List<Apple> redApples = filterApples(inventory, new ApplePredicate { ←─直接内联参数化filterapples方法的行为 public boolean test(Apple apple){ return "red".equals(apple.getColor); }});
GUI应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是Java FX API,一种现代的Java UI平台):
button.setOnAction(new EventHandler<ActionEvent> { public void handle(ActionEvent event) { System.out.println("Woooo a click!!"); }});
但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。还拿前面的例子来看,如下面高亮的代码所示:
第二,很多程序员觉得它用起来很让人费解。比如,测验2.2展示了一个经典的Java谜题,它让大多数程序员都措手不及。你来试试看吧。
测验2.2:匿名类谜题
下面的代码执行时会有什么样的输出呢,
4
、5
、6
还是42
?public class MeaningOfThis{ public final int value = 4; public void doIt { int value = 6; Runnable r = new Runnable{ public final int value = 5; public void run{ int value = 10; System.out.println(this.value); } }; r.run; } public static void main(String...args) { MeaningOfThis m = new MeaningOfThis; m.doIt; ←─这一行的输出是什么? }}
答案是
5
,因为this
指的是包含它的Runnable
,而不是外面的类MeaningOfThis
。
整体来说,啰嗦就不好;它让人不愿意使用语言的某种功能,因为编写和维护啰嗦的代码需要很长时间,而且代码也不易读。好的代码应该是一目了然的。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean
表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate
中的test
方法或是EventHandler
中的handler
方法)。
在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化。在第3章中,你会看到Java 8的语言设计者通过引入Lambda表达式——一种更简洁的传递代码的方式——解决了这个问题。好了,悬念够多了,下面简单介绍一下Lambda表达式是怎么让代码更干净的。
2.3.3 第六次尝试:使用Lambda表达式
上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor));
不得不承认这代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。我们现在已经解决了啰嗦的问题。图2-4对我们到目前为止的工作做了一个小结。
图 2-4 行为参数化与值参数化
2.3.4 第七次尝试:将List
类型抽象化
在通往抽象的路上,我们还可以更进一步。目前,filterApples
方法还只适用于Apple
。你还可以将List
类型抽象化,从而超越你眼前要处理的问题:
public interface Predicate<T>{ boolean test(T t);}public static <T> List<T> filter(List<T> list, Predicate<T> p){ ←─引入类型参数T List<T> result = new ArrayList<>; for(T e: list){ if(p.test(e)){ result.add(e); } } return result;}
现在你可以把filter
方法用在香蕉、桔子、Integer
或是String
的列表上了。这里有一个使用Lambda表达式的例子:
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor));List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
酷不酷?你现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!
2.4 真实的例子
你现在已经看到,行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple
的不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿名类一起使用。我们会展示三个例子,这应该能帮助你巩固传递代码的思想了:用一个Comparator
排序,用Runnable
执行一个代码块,以及GUI事件处理。
2.4.1 用Comparator
来排序
对集合进行排序是一个常见的编程任务。比如,你的那位农民朋友想要根据苹果的重量对库存进行排序,或者他可能改了主意,希望你根据颜色对苹果进行排序。听起来有点儿耳熟?是的,你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在Java 8中,List
自带了一个sort
方法(你也可以使用Collections.sort
)。sort
的行为可以用java.util.Comparator
对象来参数化,它的接口如下:
// java.util.Comparatorpublic interface Comparator<T> { public int compare(T o1, T o2);}
因此,你可以随时创建Comparator
的实现,用sort
方法表现出不同的行为。比如,你可以使用匿名类,按照重量升序对库存排序:
inventory.sort(new Comparator<Apple> { public int compare(Apple a1, Apple a2){ return a1.getWeight.compareTo(a2.getWeight); }});
如果农民改了主意,你可以随时创建一个Comparator
来满足他的新要求,并把它传递给sort
方法。而如何进行排序这一内部细节都被抽象掉了。用Lambda表达式的话,看起来就是这样:
inventory.sort( (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight));
现在暂时不用担心这个新语法,下一章我们会详细讲解如何编写和使用Lambda表达式。
2.4.2 用Runnable
执行代码块
线程就像是轻量级的进程:它们自己执行一个代码块。但是,怎么才能告诉线程要执行哪块代码呢?多个线程可能会运行不同的代码。我们需要一种方式来代表稍候执行的一段代码。在Java里,你可以使用Runnable
接口表示一个要执行的代码块。请注意,代码不会返回任何结果(即void
):
// java.lang.Runnablepublic interface Runnable{ public void run;}
你可以像下面这样,使用这个接口创建执行不同行为的线程:
Thread t = new Thread(new Runnable { public void run{ System.out.println("Hello world"); }});
用Lambda表达式的话,看起来是这样:
Thread t = new Thread( -> System.out.println("Hello world"));
2.4.3 GUI事件处理
GUI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单击或在文字上悬停。例如,如果用户单击“发送”按钮,你可能想显示一个弹出式窗口,或把行为记录在一个文件中。你还是需要一种方法来应对变化;你应该能够作出任意形式的响应。在JavaFX中,你可以使用 EventHandler
,把它传给setOnAction
来表示对事件的响应:
Button button = new Button("Send");button.setOnAction(new EventHandler<ActionEvent> { public void handle(ActionEvent event) { label.setText("Sent!!"); }});
这里,setOnAction
方法的行为就用EventHandler
参数化了。用Lambda表达式的话,看起来就是这样:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
2.5 小结
以下是你应从本章中学到的关键概念。
行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。