第21条:用函数对象表示策略
有些语言支持函数指针(function pointer)、代理(delegate)、lambda表达式(lambda expression),或者支持类似的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种的能力。这种机制通常允许函数的调用者通过传入第二个函数,来指定自己的行为。例如,C语言标准库中的qsort
函数要求用一个指向comparator(比较器)函数的指针作为参数,它用这个函数来比较待排序的元素。比较器函数有两个参数,都是指向元素的指针。如果第一个参数所指的元素小于第二个参数所指的元素,则返回一个负整数;如果两个元素相等则返回零;如果第一个参数所指的元素大于第二个参数所指的元素,则返回一个正整数。通过传递不同的比较器函数,就可以获得各种不同的排列顺序。这正是策略(Strategy)模式[Gamma,p.315]的一个例子。比较器函数代表一种为元素排序的策略。
Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象(that object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other objects)(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)。例如,考虑下面的类:
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
这个类导出一个带两个字符串参数的方法,如果第一个字符串的长度比第二个的短,则返回一个负整数;如果两个字符串的长度相等,则返回零;如果第一个字符串比第二个的长,则放回一个正整数。这个方法是一个比较器,它根据长度来给字符串排序,而不是根据更常用的字典顺序。指向StringLengthComparator
对象的引用可以被当作是一个指向该比较器的“函数指针(function pointer)”,可以在任意一对字符串上被调用。换句话说,StringLengthComparator
实例是用于字符串比较操作的具体策略(concrete strategy)。
作为典型的具体策略类,StringLengthComparator
类是无状态的(stateless):它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作为一个 Singleton 是非常合适的,可以节省不必要的对象创建开销(见第3条和第5条):
class StringLengthComparator {
private StringLengthComparator() { }
public static final StringLengthComparator
INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
为了把StringLengthComparator
实例传递给方法,需要适当的参数类型。使用StringLengthComparator
并不好,因为客户端将无法传递任何其他的比较策略。相反,我们需要定义一个Comparator
接口,并修改StringLengthComparator
来实现这个接口。换句话说,我们在设计具体的策略类时,还需要定义一个策略接口(strategy interface),如下所示:
// Strategy interface
public interface Comparator<T> {
public int compare(T t1, T t2);
}
Comparator
接口的这个定义碰巧也出现在java.util
包中,但是这并不神奇;你自己也完全可以定义它。Comparator
接口是泛型(见第26条)的,因此它适合作为除字符串之外的其他对象的比较器。它的compare
方法的两个参数类型为T
(它正常的参数类型),而不是String
。只要前面所示的StringLengthComparator
类要这么做,就可以用它实现Comparator<String>
接口:
class StringLengthComparator implements Comparator<String> {
... // class body is identical to the one shown above
}
具体的策略类往往使用匿名类声明(见第22条)。下面的语句根据长度对一个字符串数组进行排序:
Arrays.sort(stringArray, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数对象存储到一个私有的静态final
域里,并重用它。这样做的另一种好处是,可以为这个函数对象取一个有意义的域名称。
因为策略接口被用作所有具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。相反,“宿主类(host class)”还可以导出公有的静态域(或者静态工厂方法),其类型为策略接口,具体的策略类可以是宿主类的私有嵌套类。下面的例子使用静态成员类,而不是匿名类,以便允许具体的策略类实现第二个接口Serializable
:
// Exporting a concrete strategy
class Host {
private static class StrLenCmp
implements Comparator<String>, Serializable {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
// Returned comparator is serializable
public static final Comparator<String>
STRING_LENGTH_COMPARATOR = new StrLenCmp();
... // Bulk of class omitted
}
String
类利用这种模式,通过它的CASE_INSENSITIVE_ORDER
域,导出一个不区分大小写的字符串比较器。
简而言之,函数指针的主要用途就是实现策略(Strategy)模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略类是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final
域被导出,其类型为该策略接口。