在软件工程中,一个个周所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又ᡪ回来补充道:“要是我可以找出所有既是绿色苹果,重量也超过150克的苹果,那就太棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。
这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果你要处理一个集合,可能会写一个方法:
(1)可以对列表中的每个元素做“某件事”
(2)可以在列表处理完后做“另一件事”
(3)遇到错误时可以做“另外一件事”
行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去˻一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取包裹。你可以把这些指示用电子邮件件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go,它可以接受不同的新行为作为参数,然后去执行。
这一章首先会给你讲解一个例子,说明如何对你的代码加以改进,从而更灵活地适应不断变化的需求。在此基础之上,我们将展示如何把行为参数化用在几个真实的例子上。比如,你可能已经用过了行为参数化模式——使用Java API中现有的类和接口,对List进行排序,筛选文件名,或告诉一个Thread去执行代码块,甚或是处理GUI事件。你很快会发现,在Java中使用这种模式十分啰嗦。Java 8中的Lambda解决了代码啰嗦的问题。我们会在第3章中向你展示如何构建Lambda表达式、其使用场合,以及如何利用它让代码更简洁。

1.应对不断变化的需求

编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。听起来很简单吧?

1.1.1 初试牛刀:筛选绿苹果

// 第一个解决方案
    public 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;
    }

1.1.2 再展身手:把颜色作为参数

// 第二个解决方案
    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。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!我们很快会解释为什么)

1.1.3 再展身手:对你能想到的每个属性 做筛选

// 笨拙的处理方式
    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);

这个解决方案再差不过了。首先,客户端代码看上去ጁ透了。true和false是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。到目前为止,你已经给filterApples方法加上了值(比如String、Integer或boolean)
的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。在下一节中,我们会介绍了如何利用行为参数化实现这种灵活性。

1.2 行为参数化

你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选筛选标准建模:

package com.learn.java8.predicate;

import com.learn.java8.model.Apple;

public interface ApplePredicate {

    // true 符合条件 false 不符合条件
    boolean test(Apple apple);
}
class AppleHeavyWeightPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple){
        return apple.getWeight() > 150;
    }
}

class AppleGreenColorPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple){
        return "green".equals(apple.getColor());
    }
}

这里的箭头是“接口与实现的关系”

Java通过按钮切换升序还是降序_Apple


你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接收多种行为(或策略)作为参数,并在内部使用,来完成不同的行为。

然后我们对ApplePredicate类就可以进行抽象编程了。

// 利用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

类,从而去掉不必要的代码。

Java通过按钮切换升序还是降序_Java通过按钮切换升序还是降序_02


(2) 多种行为,一个参数

正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如图2-3所示。

Java通过按钮切换升序还是降序_List_03


这就是行为参数化一个有用的概念,应该把它放进你的工具箱,用来编写灵活的API。

小测试

编写灵活的prettyPrintApple方法

Java通过按钮切换升序还是降序_List_04


Java通过按钮切换升序还是降序_Java通过按钮切换升序还是降序_05

总结

过程较为啰嗦,不得不声明好几个类继承ApplePredicate接口的类,然后会实例化好几个提到一次其对象。

Java通过按钮切换升序还是降序_Java通过按钮切换升序还是降序_06

1.3 匿名类

匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。

1.3.1 使用匿名类

使用一个随建随用的类

// 下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:
    public static List<Apple> filterApples(List<Apple> inventory) {
        List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        });
        return redApples;
    }

Java通过按钮切换升序还是降序_Apple_07


Java通过按钮切换升序还是降序_参数化_08


Java通过按钮切换升序还是降序_java_09

缺点:声明的类少了,但是创建的对象还是这么多。

1.3.2 使用lamdba

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

Java通过按钮切换升序还是降序_List_10

1.3.3 将List类型抽象化

可能,我们的农名并不需要筛选苹果了,他们可能需要筛选香蕉、橘子、等其他水果。这时候,我们就需要将我们的List抽象化了。
使用泛型

package com.learn.java8.predicate.demo;

import com.learn.java8.model.Apple;

import java.util.ArrayList;
import java.util.List;

public interface Predicate<T> {

    // true 符合条件 false 不符合条件
    boolean test(T model);
    
    // 参数抽象化
    public static <T> List<T> filet(List<T> inventory,Predicate<T> predicate){
        List<T> result = new ArrayList<>();
        for (T item:inventory){
            if (predicate.test(item)){
                result.add(item);
            }
        }
        return result;
    }
}

1.4 真实的例子

你现在已经看到,行为参数化是一个很有用的模式,它能够轻松地应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿名类一起使用。我们会展示三个例子,这应该能帮助你巩固传递代码的思想了:用一个Comparator排序,用Runnable执行一个代码块,以及GUI事件处理。

1.4.1 用Comparator 来排序

对集合进行排序是一个常见的编程任务。比如,你的那位农民朋友想要根据苹果的重量对库存进行排序,或者他可能改了主意,希望你根据颜色对苹果进行排序。听起来有点儿耳熟?是的,你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。

在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:

Java通过按钮切换升序还是降序_参数化_11


Java通过按钮切换升序还是降序_java_12


lamdba表达式

public static void sortByWeight(List<Apple> inventory){
        inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
    }

1.4.2 用Runnable 执行代码块

线程就像是轻量级的进程:它们自己执行一个代码块。但是,怎么才能告诉线程要执行哪块代码呢?多个线程可能会运行不同的代码。我们需要一种方式来代表稍候执行的一段代码。在Java里,你可以使用Runnable接口表示一个要执行的代码块。请注意,代码不会返回任何结果(即void):

Java通过按钮切换升序还是降序_java_13

1.4.3 GUI事件处理

GUI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单机或在文字上悬停。

例如,如果用户单机“发送”按钮,你可能想显示一个弹出式窗口,或把行为记录在一个文件中。你还是需要一种方法来应对变化;你应该能够作出任意形式的响应。在JavaFX中,你可以使用EventHandler,把它传给setOnAction来表示对事件的响应:

Java通过按钮切换升序还是降序_Java通过按钮切换升序还是降序_14

总结

Java通过按钮切换升序还是降序_Apple_15

下一章我们将来手写自己的lamdba