什么是谓词?什么是行为参数化?
java8增加了把方法(你的代码)作为参数传递给另一个方法的能力。
背景:应对不断变化的需求
筛选绿苹果
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);
}
}
}
如果想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
将颜色作为参数
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterGreenApples(List<Apple> inventory,String color){
List<Apple> result = new ArrayList();
for(Apple apple:inventory){
if(color.equals(apple.getColor())){
result.add(apple);
}
}
}
如果要能区分轻的苹果和重的苹果怎么办?例如筛选出重量大于150g的苹果?用另一个参数应对?
public static List<Apple> filterGreenApples(List<Apple> inventory,int weight){
List<Apple> result = new ArrayList();
for(Apple apple:inventory){
if(color.getWeight() > weight){
result.add(apple);
}
}
}
以上代码有哪些问题?
- 打破DRY(Don’t Repeat Yourself)
- 修改筛选方式就意味着要修改所有方法的实现
- 绝对不要加上一个标志来区分对颜色和重量的查询
行为参数化
现在需要一种比添加很多参数更好的方法来应对变化的需求。
一种好的方案是对你的选择标准建模,需要根据Apple的某些属性来返回一个boolean值,我们把它称为谓词(即一个返回boolean值的函数)。
我们来定义一个接口来对选择标准建模:
public interface ApplePredicate{
boolean test(Apple apple);
}
现在可以使用ApplePredicate的多个实现代表不同的选择标准了,比如:
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWight()>150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
可以将这些标准看做filter方法的不同行为。如上是一个策略设计模式,该怎么利用ApplePredicate的不同实现呢?需要filterApples方法接受ApplePredicate对象,对Apple做条件测试,这就是参数行为化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
根据抽象条件筛选
利用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;
}
传递代码行为
想在可以创建不同的ApplePredicate对象,并将他们传递给filterApples方法。比如如果让你找出所有重量超过150g的红苹果,只需要创建一个类来实现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方法的实现,正是它定义了filterApples方法的新行为。令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。
通过使用lambda,你可以直接把表达式(“red”.equals(apple.getColor()) && apple.getWeight()>150)传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。
参数化filterApples的行为,传递不同的筛选策略(ApplePredicate p)
多种行为,一个参数
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样可以重复使用同一个方法,给它不同的行为来达到不同的目的。
对付啰嗦
目前,当要把新的行为传递给filterApples方法的时候,不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。
费这么大劲真没必要,如何做的更好呢?如何同时声明和实例化一个类?
匿名类
匿名类和java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。
使用匿名类
List<Apple> redApple = filterApples(inventory,new ApplePredicate(){
//直接内联参数化filterApples方法的行为
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
但匿名类还是不够好:
- 它往往很笨重,因为它占用了很多空间。
- 很多程序员觉得它用起来很让人费解。
即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。
在只需要传递一段简单的代码时(例如选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为。
Lambda表达式
上面的代码可以使用Lambda表达式重写为:
List<Apple> result = filterApples(inventory,(Apple apple) -> "red".equals(apple.getColor()));
这段代码看上去比之前干净多了,因为它看起来更像问题陈述本身了。
将List类型抽象化
在通往抽象的路上,我们还可以更进一步,目前,filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:
public interface Predicate<T>{
boolean test(T t);
}
pulic static <T> List<T> filter(List<T> list,Predicate<T> P){
List<T> result = new ArrayList();
for(T e:list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
现在可以把filter方法用在香蕉、桔子、Integer或者String的列表上了,如下所示:
List<Apple> redApples = filter(inventory,(Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers = filter(inventory,(Integer i) -> i % 2 == 0);
现在在灵活性和简洁性之间找到了最佳平衡点,是不是很酷?
实战
用Comparator来排序
对集合排序是一个常见的编程任务。我们可以使用一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在java8中List自带了一个sort方法(也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:
public interface Comparator<T>{
public int compare(T o1,T o2);
}
用Lambda表达式:
inventory.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
用Runnable执行代码块
线程就像是轻量级的进程:它们自己执行一个代码块,可以使用Runnbale接口表示一个要执行的代码块(void代表代码不会返回任何结果)。
public 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"));
总结
- 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力
- 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量
- 传递代码,就是将新行为作为参数传递给方法。
- Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程等。