考虑编写一种方法,该方法采用一个对象数组和一个集合,并将该数组中的所有对象放入集合中。这是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // compile-time error
}
}
到现在为止,您已经学会了避免初学者尝试将Collection<Object>
用作collection参数类型的错误。您可能会,也可能不会认识到,使用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!
}
但是,在containsAll
和addAll
中,类型参数T
仅使用一次。返回类型不依赖于类型参数,也不依赖于该方法的任何其他参数(在这种情况下,仅存在一个参数)。这告诉我们类型参数用于多态。它的唯一作用是允许在不同的调用站点使用各种实际的参数类型。在这种情况下,应使用通配符。通配符旨在支持灵活的子类型化,这就是我们在此要表达的内容。
泛型方法允许使用类型参数来表示方法的一个或多个参数的类型和/或其返回类型之间的依赖性。如果没有这种依赖性,则不应使用泛型方法。
可以同时使用泛型方法和通配符。这是方法Collections.copy()
:
class Collections {
public static <T> void copy(List<T> dst, List<? extends T> src) {
...
}
注意两个参数的类型之间的依赖关系。从源列表src
复制的任何对象都必须可分配给目标列表dst
的元素类型T
。因此,src
的元素类型可以是T
的任何子类型——我们不在乎。copy
的签名使用类型参数表示依赖性,但对第二个参数的元素类型使用通配符。
我们可以以另一种方式编写此方法的签名,而根本不使用通配符:
class Collections {
public static <T, S extends T> void copy(List<T> dst, List<S> src) {
...
}
很好,虽然在第二类型参数的类型dst
和边界中都使用了第一类型参数S
,但S
本身仅被使用一次,而第二类型参数src
的类型中没有其他依赖。这表明我们可以用通配符代替S
。使用通配符比声明显式类型参数更清晰,更简洁,因此应尽可能使用通配符。
通配符还具有可以在方法签名之外用作字段,局部变量和数组的类型的优点。这是一个例子。
回到我们的形状绘制问题,假设我们要保留绘制请求的历史记录。我们可以将历史记录保存在Shape
类内的静态变量中,drawAll()
将其传入参数存储到history字段中。
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
。如果泛型方法出现在泛型类中,则最好避免对方法和类的类型参数使用相同的名称,以免造成混淆。嵌套泛型类也是如此。