Generic Methods

考虑编写一种方法,该方法采用一个对象数组和一个集合,并将该数组中的所有对象放入集合中。这是第一次try:

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

到现在为止,您已经学会了避免 Starters 的错误,即try使用Collection<Object>作为 collection 参数的类型。您可能已经或没有意识到使用Collection<?>也不起作用。回想一下,您不能仅将对象推入未知类型的集合中。

解决这些问题的方法是使用泛型方法。就像类型声明一样,方法声明可以是通用的,即由一个或多个类型参数进行参数化。

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仅使用一次。返回类型不依赖于 type 参数,也不依赖于该方法的任何其他参数(在这种情况下,仅存在一个参数)。这告诉我们类型参数用于多态。它的唯一作用是允许在不同的调用站点使用各种实际的参数类型。在这种情况下,应使用通配符。通配符旨在支持灵活的子类型化,这就是我们在此要表达的内容。

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

可以同时使用泛型方法和通配符。这是方法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本身仅使用一次,其他都取决于它。这表明我们可以用通配符替换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。如果泛型方法出现在泛型类中,则最好避免对方法和类的类型参数使用相同的名称,以免造成混淆。嵌套泛型类也是如此。