Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
考虑编写一个方法,该方法接受一个对象数组和一个集合,并将数组中的所有对象放入集合中。这是第一次尝试:
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! }
但是,在 containsAll
和 addAll
中,类型形参 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
。如果泛型方法出现在泛型类中,最好避免对方法和类的类型形参使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。