通用方法

原文: https://docs.oracle.com/javase/tutorial/extra/generics/methods.html

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

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

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

处理这些问题的方法是使用泛型方法。就像类型声明一样,方法声明可以是通用的 - 也就是说,由一个或多个类型参数参数化。

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

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

  1. Object[] oa = new Object[100];
  2. Collection<Object> co = new ArrayList<Object>();
  3. // T inferred to be Object
  4. fromArrayToCollection(oa, co);
  5. String[] sa = new String[100];
  6. Collection<String> cs = new ArrayList<String>();
  7. // T inferred to be String
  8. fromArrayToCollection(sa, cs);
  9. // T inferred to be Object
  10. fromArrayToCollection(sa, co);
  11. Integer[] ia = new Integer[100];
  12. Float[] fa = new Float[100];
  13. Number[] na = new Number[100];
  14. Collection<Number> cn = new ArrayList<Number>();
  15. // T inferred to be Number
  16. fromArrayToCollection(ia, cn);
  17. // T inferred to be Number
  18. fromArrayToCollection(fa, cn);
  19. // T inferred to be Number
  20. fromArrayToCollection(na, cn);
  21. // T inferred to be Object
  22. fromArrayToCollection(na, co);
  23. // compile-time error
  24. fromArrayToCollection(na, cs);

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

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

  1. interface Collection<E> {
  2. public boolean containsAll(Collection<?> c);
  3. public boolean addAll(Collection<? extends E> c);
  4. }

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

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

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

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

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

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

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

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

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

这很好,但是第一个类型参数既用于dst类型又用于第二个类型参数的边界,SS本身仅使用一次,类型为src - 没有别的东西取决于它。这表明我们可以用通配符替换S。使用通配符比声明显式类型参数更清晰,更简洁,因此应尽可能优先使用通配符。

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

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

  1. static List<List<? extends Shape>>
  2. history = new ArrayList<List<? extends Shape>>();
  3. public void drawAll(List<? extends Shape> shapes) {
  4. history.addLast(shapes);
  5. for (Shape s: shapes) {
  6. s.draw(this);
  7. }
  8. }

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