9.4.2 泛型方法和类型通配符的区别

9.4.2 泛型方法和类型通配符的区别

大多数时候都可以使用泛型方法来代替类型通配符。例如,对于JavaCollection接口中两个方法定义:

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

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示。

1
2
3
4
5
6
7
...
public interface Collection<E> extends Iterable<E> {
...
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}

上面方法使用了<T extends E>泛型形式,这时定义泛型形参时设定上限(其中ECollection接口里定义的泛型,在该接口里E可当成普通类型使用)。

上面两个方法中泛型形参T只使用了一次,泛型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声眀不应该使用通配符——因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明泛型,也就是泛型方法。

如果有需要,也可以同时使用泛型方法和通配符,如JavaCollections.copy()方法。

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

上面copy方法中的destsrc存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法。

提示:简而言之,指定上限的类型通配符支持协变,因此**这种协变的集合可以安全地取出元素(协变只出不进)**,因此无须使用泛型方法。

当然,也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如下所示

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

这个方法签名可以代替前面的方法签名。但注意上面的泛型形参S,它仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那泛型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰和准确,因此Java设计该方法时采用了通配符,而不是泛型方法。

类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法中显式声明。