文档

Java™ 教程-Java Tutorials 中文版
泛型方法
Trail: Bonus
Lesson: Generics

泛型方法

考虑编写一个方法,该方法接受一个对象数组和一个集合,并将数组中的所有对象放入集合中。这是第一次尝试:

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        c.add(o); // compile-time error
    }
}

到目前为止,你将学会避免初学者错误地尝试使用 Collection<Object> 作为集合参数的类型。你可能已经或可能没有认识到使用 Collection<?> 也无法正常工作。回想一下,不能将对象推送到未知类型的集合中。

处理这些问题的方法是使用 generic methods (泛型方法)。就像类型声明一样,方法声明可以是泛型的,即由一个或多个类型形参参数化。

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // Correct
    }
}

我们可以使用任何类型的集合调用此方法,只要其元素类型是数组元素类型的超类型。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T inferred to be Object
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T inferred to be String
fromArrayToCollection(sa, cs);

// T inferred to be Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T inferred to be Number
fromArrayToCollection(ia, cn);

// T inferred to be Number
fromArrayToCollection(fa, cn);

// T inferred to be Number
fromArrayToCollection(na, cn);

// T inferred to be Object
fromArrayToCollection(na, co);

// compile-time error
fromArrayToCollection(na, cs);

请注意,我们不必将实际类型实参传递给泛型方法。编译器根据实际参数的类型为我们推断类型实参。它通常会推断出使调用类型正确的最特定的类型实参。

出现的一个问题是:何时应该使用泛型方法,何时应该使用通配符类型?为了理解答案,我们来看一下 Collection 库中的一些方法。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

我们可以在这里使用泛型方法:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

但是,在 containsAlladdAll 中,类型形参 T 仅使用一次。返回类型不依赖于类型形参,也不依赖于方法的任何其他参数(在这种情况下,只有一个参数)。这告诉我们类型实参用于多态;它唯一的作用是允许在不同的调用点使用各种实际的参数类型。如果是这种情况,则应使用通配符。通配符旨在支持灵活的子类型,这是我们在此尝试表达的内容。

泛型方法允许使用类型形参来表示方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。如果没有这种依赖关系,则不应使用泛型方法。

可以串联使用泛型方法和通配符。这是方法 Collections.copy()

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意两个参数类型之间的依赖关系。从源列表 src 复制的任何对象必须可分配给目标列表 dst 的元素类型 T。所以 src 的元素类型可以是 T 的任何子类型,我们不关心是哪一个。copy 的签名使用类型形参表示依赖关系,但对第二个参数的元素类型使用通配符。

我们可以用另一种方式为这种方法编写签名,而不使用通配符:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这很好,但是第一个类型形参既用于 dst 的类型,也用于第二个类型形参的边界 S,但 S 本身只使用一次,在 src 的类型中没有别的依赖它。这表明我们可以用通配符替换 S。使用通配符比声明显式类型形参更清晰,更简洁,因此应尽可能优先使用通配符。

通配符还具有以下优点:它们可以在方法签名之外使用,如字段类型,局部变量和数组。这是一个例子。

回到我们的形状绘制问题,假设我们想要保留绘图请求的历史记录。我们可以在类 Shape 中的静态变量中维护历史记录,并让 drawAll() 将其传入的参数存储到历史记录字段中。

static List<List<? extends Shape>> 
    history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

最后,再次让我们注意用于类型形参的命名规范。我们使用 T 作为类型,只要没有更具体的类型来区分它。泛型方法通常就是这种情况。如果有多个类型形参,我们可能会使用字母表中与 T 相邻的字母,例如 S。如果泛型方法出现在泛型类中,最好避免对方法和类的类型形参使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。


Previous page: Wildcards
Next page: Interoperating with Legacy Code