前言

本文并不是要从头开始介绍泛型,只要是对Java稍有了解的人大概都不会对泛型感到陌生,如果对泛型还完全没有过了解,可以参考这两篇文章。

Java泛型就是这么简单

Java帝国之泛型

Java官方教程-通配符

而本文主要是小结一下学习泛型时遇到的一些疑惑。

通配符的疑问

基本上只要是扯到通配符的都没搞清楚过,比如说: 1. 通配符到底是干嘛用的? 2. 它和常用的泛型T之间有什么区别? 3. 上界通配符(? extends xxx)和下界通配符(? super xxx)又是干什么用的? 4. 同样是extends, T extends xxx 和 ? extends xxx 又有什么区别? 5. ? super T 又是啥? 6. 为什么需要Class>, 我直接用Class不行吗?

文章顺序和问题顺序并非一致

基本概念

在讲通配符之前,有几个概念和知识点还是得先搞清楚,后面说起来方便些:

通配符概念ArrayList中的E称为类型参数变量

ArrayList中的Integer称为实际类型参数

整个称为ArrayList泛型类型

整个ArrayList称为参数化的类型ParameterizedType

-- java3y《泛型就是这么简单》

向上转型

当我需要一个父类的时候,我都可以用一个子类来代替它 1. 例如一个方法需要返回父类,我也可以返回一个子类对象 2. 我可以向List中插入一个Child对象

通配符的作用

以下是我对通配符的一些理解: 1. 通配符是用来解决泛型无法协变的问题的

协变指的就是如果Child是Parent的子类,那么List 也应该是List 的子类。但是泛型是不支持的,至于原因,可参见上面提到的Java帝国之泛型 2. 泛型T是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围 3. 通配符不是类型参数变量,或者说通配符和类型参数变量T压根就不是一个东西。

你可以理解成泛型T就像是个变量,等着你将来传一个具体的类型拿来用的,而通配符则是一种规定,规定你能传哪些参数。

你甚至可以理解成通配符就像是一个特殊的实际类型参数。

理解这一点对于理解通配符非常重要。

代码分析word is cheap , show me the code

还是拿比较经典的Fruit List来举例子 现在有下面三个类

Fruit

Apple extends Fruit

Orange extends Fruit

现在我想打印一个一个List,List中可以装Fruit或是Fruit的子类,其他的都不行,那么我们写成这样行不行呢? - 想法一

public static void print(List list) {
...
}

这样肯定不行,我们上面说过,泛型不是协变的,你这样只能传List 而不能传List了。想法二 我们都知道泛型T也是有extends的

public static void printByGenericParam(List list) {
for (T fruit : list) {
System.out.println(fruit);
}
}

想法三

使用通配符

public static void print(List extends Fruit> list) {
for (Fruit fruit : list) {
System.out.println(fruit);
}
}

这里可以看到无论是类型参数变量还是通配符都可以达到我们想要的效果。

但是通配符无须在参数上进行任何声明(比如),它只是在参数上进行了规定,使你可以传递Fruit的子类。

你甚至可以理解成下面这样:

类型参数变量T:

T -> T extends Fruit // 对T进行了限制,缩小了T的范围

通配符:

Fruit -> ? extends Fruit // 对参数Fruit进行了规定,允许你传递Fruit的子类,扩大了Fruit的范围

不同点:

虽然他们都可以完成功能,但其中还是稍微有一些差异。

泛型T: 例如传一个Apple,那么它的类型就是Apple

通配符:例如传一个Apple,它的声明类型实际上是一个Fruit,使用了多态才能调用Apple的toString()方法。

?和 T的区别

?相对于T来说有很多的限制,比如说: 1. 上面返回的是void,那如果我想传入什么就返回什么,那通配符是做不到的 2. 后面会讲到上界/下面通配符,List配合这两种通配符时,一个不能修改,一个不能读取

但是通配符也有类型参数做不到的: 1. 通配符可以使用?super(即下界通配符)

? extends/super T

这个语法其实也很好的证明了类型参数和通配符的不同之处,以 ? super T 为例:

其中这个T就意味着你将来会传进来一个具体的参数,例如你传进来一个Apple,那么这个式子就变成了? super Apple,而通配符的作用就是扩大了Apple的范围,把它变成了Apple或者其父类

代码分析:

例如我们现在有一个求最小值函数。 1. 因为我们需要返回具体的类型,所以只能使用类型参数 2. 因为sort方法需要 T 实现Comparable接口,所以在泛型参数中需要添加T extends Comparable

public static > T min(List list) {
Collections.sort(list);
return list.get(0);
}

现在这个代码要求我们传进去的T都实现Comparable接口。

例如传Apple进去,Apple就必须实现Comparable。

但现在如果有这样一个需求。

Fruit 实现了Comparable接口

Apple和Orange都没有实现Comparable接口

现在我希望Apple和Orange统一的使用Fruit的compareTo方法

此时类型参数似乎就搞不定了,此时就到了通配符上场了:

Comparable

变成

Comparable super T>
public static > T min(List list) {
Collections.sort(list);
return list.get(0);
}
public static void main(String[] args) {
List appleList = new ArrayList<>();
appleList.add(new Apple());
min(appleList);
}

PECS

? extends/super 在前面已经讲过了,但是当它和集合一起使用的时候还有一些限制。下面我们都是使用List作为例子。

基本概念PECS:Producer extends Consumer super (生产者使用extends,consumer使用super)List extends T>只能往外面拿,不能往里面写

List super T>只能往里面写,不能往外面拿 这一部分网上的讲解非常多并且很清楚,这里简述一下自己的理解。

上界通配符

List extends Fruit>
public void print(List extends Fruit> list) {
...
}

那么你的集合里面可能装的是 Apple,Orange,Fruit

Fruit

list -> Apple

Orange不能写

你可能传进去的是个List,那你还能往里面随便丢个Orange进去吗?

可以读

你取出来的都只能被当成Fruit,无论你取出来的是Apple还是Orange,根据向上转型,你都可以把它当成是Fruit,但你无法确定它具体是哪一种类型,所以被取出来的只能当做Fruit处理

下界通配符

List super Fruit>

如果上面能够理解,那么这个也就很好理解了。

public void add(List super Fruit> list){
...
}

那么你的集合里面可能装的是 Apple,Orange,Fruit

Fruit

list -> Food

Obejct可以写

那么无论你传进去的是List,还是List,我扔个Apple进去总没问题吧

不能读

我无法确定我取出来的到底是什么,同时我也无法进行转型,假如你传进来的是个List,我总不能强转成Fruit吧

Class>

这个就比较让人头疼了,我们前面说通配符的作用是对参数进行规定,使得它可以传递其子类或者父类从而解决泛型无法协变的问题。

但是你一个? 却什么都不加你是想干嘛,我为什么不直接使用Class而非要使用Class>呢?

实际上它俩在功能上也确实没有什么区别,只是不使用通配符会有警告

Class clazz1 = String.class; // 会报rowTypes的warning警告

Class> clazz2 = String.class;

因为Class在定义的时候使用了泛型,所以如果什么都不传,它会认为你使用了没有类型的Class,就好比你使用了一个无类型的List

List list = new ArrayList();

使用场景

这里就直接套用Java官方教程中的话了。The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.常用于 :

可以被用于类型参数中,(比如上面讲到的min函数的例子)。

也可以被用于实例变量或者是局部变量(比如上面提到的Class>的例子)

有时也作为返回类型,比如Object的getClass方法。

不用于:

泛型方法调用时作为类型参数使用(就是说它不能跟泛型T似的,你传什么进去就能确定并使用这个T,甚至还可以返回它

不用于接口或者类的创建(个人的理解就是我们在定义类的时候一般不会用通配符而是使用泛型T)