文档

Java™ 教程
隐藏目录
泛型方法
路径: 奖励
课程: 泛型

泛型方法

`作为集合参数的类型。你可能已经认识到,使用`Collection`也行不通。请记住,不能将对象随意放入未知类型的集合中。 解决这些问题的方法是使用泛型方法。与类型声明一样,方法声明也可以是泛型的,即可以由一个或多个类型参数参数化。 ```java static void fromArrayToCollection(T[] a, Collection c) { for (T o : a) { c.add(o); // 正确 } } ``` 我们可以使用任何元素类型是数组元素类型的超类型的集合来调用这个方法。 ```java Object[] oa = new Object[100]; Collection co = new ArrayList(); // 推断T为Object fromArrayToCollection(oa, co); String[] sa = new String[100]; Collection cs = new ArrayList (); // 推断T为String fromArrayToCollection(sa, cs); // 推断T为Object fromArrayToCollection(sa, co); Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection cn = new ArrayList (); // 推断T为Number fromArrayToCollection(ia, cn); // 推断T为Number fromArrayToCollection(fa, cn); // 推断T为Number fromArrayToCollection(na, cn); // 推断T为Object fromArrayToCollection(na, co); // 编译时错误 fromArrayToCollection(na, cs); ``` 请注意,我们不必向泛型方法传递实际的类型参数。编译器根据实际参数的类型为我们推断类型参数。它通常会推断出使调用类型正确的最具体的类型参数。 一个问题是,什么时候应该使用泛型方法,什么时候应该使用通配符类型?为了理解答案,让我们来看一下`Collection`库中的一些方法。 ```java interface Collection { public boolean containsAll(Collection c); public boolean addAll(Collection c); } ```

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

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // 嘿,类型变量也可以有边界!
}

然而,在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。如果泛型方法出现在泛型类中,最好避免在方法和类的类型参数中使用相同的名称,以避免混淆。嵌套的泛型类也是如此。


上一页: 通配符
下一页: 与传统代码互操作