第27条:优先考虑泛型方法

就如类可以从泛型中收益一般,方法也一样。静态工具方法尤其适合于泛型化。Collections中的所有“算法”方法(例如binarySearchsort)都泛型化了。

编写泛型方法与编写泛型类相类似。例如下面这个方法,它返回两个集合的联合:

// Uses raw types - unacceptable! (Item 23)
public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

这个方法可以编译,但是有两条警告:

Union.java:5: warning: [unchecked] call to
Hashset(Collection<? extends E>) as a member of raw type HashSet
        Set result = new HashSet(s1);
                     ^
Union.java:6: warning: [unchecked] call to
addAll(Collection<? extends E>) as a member of raw type Set
        result.addAll(s2);
                     ^

为了修正这些警告,使方法编程是类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型(两个参数和一个返回值),并在方法中使用类型参数。声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间。在这个示例中,类型参数列表为<E>,返回类型为Set<E>。类型参数的命名管理与泛型方法以及泛型的相同(见第 26 条和第 44 条):

// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

至少对于简单的泛型方法而言,就是这么回事了。现在该方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。以下是一个知性该方法的简单程序。程序中不包含转换,编译时不会有错误或者警告:

// Simple program to exercise generic method
public static void main(String[] args) {
    Set<String> guys = new HashSet<String>(
        Arrays.asList("Tom", "Dick", "Harry"));
    Set<String> stooges = new HashSet<String>(
        Arrays.asList("Larry", "Moe", "Curly"));
    Set<String> aflCio = union(guys, stooges);
    System.out.println(aflCio);
}

运行这段程序时,会打印出[Moe, Harry, Tom, Curly, Larry, Dick]。元素的顺序是依赖于实现的。

union方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型(bounded wildcard type),可以使这个方法变得更加灵活(见第28条)。

泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。对于上述的程序而言,编译器发现union的两个参数都是Set<String>类型,因此知道类型参数E必须为String。这个过程称作类型推导(type inference)。

如第1条所述,可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松。提醒一下:在调用泛型构造器的时候,要明确传递类型参数的值可能有点麻烦。类型参数出现在了变量声明的左右两边,显得有些冗余:

// Parameterizedtype instance creation with constructor
Map<String, List<String>> anagrams = 
    new HashMap<String, List<String>>();

为了消除这种冗余,可以编写一个泛型静态工厂方法(generic static factory method),与想要使用的每个构造器相对应。例如,下面是一个与无参的HashMap构造器相对应的泛型静态工厂方法:

// Generic static factory method
public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}

通过这个泛型静态工厂方法,可以用下面这段简洁的代码来取代上面那个重复的声明:

// Parameterizedtype instance creation with static factory
Map<String, List<String>> anagrams = newHashMap();

在泛型上调用构造器时,如果语言所有的类型推导与调用泛型方法时所做的相同,那就好了。将来的某一天也许可以实现这一点,但截至 Java 1.6 发行版还不行。

相关的模式是泛型单例工厂(generic singleton factory)。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除(见第25条)实现的,可以给所有必要的类型参数使用单个对象,但是需要编写一个静态工厂方法,重复地给每个必要的类型参数分发对象啊。这种模式最常用于函数对象(见第21条),如Collections.reverseOrder,但也适用于像Collections.emptySet这样的集合。

假设有一个接口,描述了一个方法,该方法接受和返回某个类型T的值:

public interface UnaryFunction<T> {
    T apply(T arg);
}

现在假设要提供一个恒等函数(identity function)。如果在每次需要的时候都重新创建一个,这样会很浪费,因为它是无状态的(stateless)。如果泛型被具体化了,每个类型都需要一个恒等函数,但是它们被擦除以后,就只需要一个泛型单例。请看以下示例:

// Generic singleton factory pattern
private static UnaryFunction<Object> IDENTITY_FUNCTION = 
    new UnaryFunction<Object>() {
        public Object apply(Object arg) { return arg; }
  }

// IDENTITY_FUNCTION is stateless and its type parameter is
// unbounded so it's safe to share one instance across all types.
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
}

IDENTITY_FUNCTION转换成(UnaryFunction<T>)产生了一条未受检转换警告,因为UnaryFunction<Object>对于每个T来说并非都是个UnaryFunction<T>。但是恒等函数很特殊:它返回未被修改的参数,因此我们知道无论T的值是什么,用它作为UnaryFunction<T>都是类型安全的。因此,我们可以放心地禁止由这个转换所产生的未受检转换警告。一旦禁止,代码在编译时就不会出现任何错误或者警告。

以下是一个范例程序,利用泛型单例作为UnaryFunction<String>UnaryFunction<Number>。像往常一样,它不包含转换,编译时没有出现错误或者警告:

// Sample program to exercise generic singleton
public static void main(String[] args) {
    String[] strings = { "jute", "hemp", "nylon" };
    UnaryFunction<String> sameString = identityFunction();
    for (String s : strings)
        System.out.println(sameString.apply(s));

    Number[] numbers = { 1, 2.0, 3L };
    UnaryFunction<Number> sameNumber = identityFunction();
    for (Number n : numbers) 
        System.out.println(sameNumber.apply(n));
}

虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的。这就是递归类型限制recursive type bound)。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序:

public interface Comparable<T> {
    int compareTo(T o);
}

类型参数T定义的类型,可以与实现Comparable<T>的类型的元素进行比较。实际上,几乎所有的类型都只能与它们自身的类型的元素相比较。因此,例如String实现Comparable<String>Integer实现Comparable<Integer>,等等。

有许多方法都带有一个实现Comparable接口的元素列表,为了对列表进行排序,并在其中进行搜索,计算它的最小值或者最大值,等等。要完成这其中的任何一项工作,要求列表中的每个元素都能够与列表中的每个其他元素相比较,换句话说,列表的元素可以互相比较(mutually comparable)。下面是如何表达这种约束条件的一个示例:

// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {...}

类型限制<T extends Comparable<T>>可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有些一致。

下面的方法就带有上述声明。它根据元素的自然顺序计算列表的最大值,编译时没有出现任何错误或者警告:

// Returns the maximum value in a list - uses recursive type bound
public static <T extends Comparable<T>> T max(List<T> list) {
    Iterator<T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()) {
        T t = i.next();
        if (t.compareTo(result) > 0)
            result = t;
    }
    return result;
}

递归类型限制可能比这个要复杂得多,但幸运的是,这种情况并不经常发生。如果你理解了这种习惯用法及其通配符变量(见第28条),就能够处理在实践中遇到的许多递归类型限制了。

总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端(见第23条)。

results matching ""

    No results matching ""