不诗意的女程序媛不是好厨师~
转载请注明出处,From李诗雨---https://blog.csdn.net/cjm2484836553/article/details/103302194】
在 《重学Java系列》之 泛型(上)中我们已经知道了①什么是泛型,②为什么要有泛型,③泛型类、泛型接口 和泛型方法,④给泛型变量增加约束,⑤泛型类型的继承规则。
今天要讲的就是有一点点难度的东西了,理解一个东西最好的方式就是: 自己找到相似的情景作类比→画图帮助理解→自己敲代码理解→反思回味。 毕竟只有经过自己大脑思考的东西,才能为己所用。
笔记我还是整理的很细,也很好理解。那么今天,让我来继续学习剩下的三点内容吧~♥
6.泛型通配符
你见过上面☝这样的代码吗
? 和关键字extends或者super在一起其实就是泛型的高级应用:通配符。
- ? extends X 表示类型的上界,类型参数是X的子类
- ? super X 表示类型的下界,类型参数是X的超类
对于它们的使用送大家八字咒语:
“上界不存下界不取”
这个咒语什么意思,别急让我来慢慢讲:
(1). 先看看 ? extends X
它表示传递给方法的参数,必须是X的子类(包括X本身),
是类型的上界。
所以上界 就是 针对 ?extends X 来说的。
我们来让代码自己说它是什么意思~
有这样的几个类:
public class Animal {
//...
}
public class Pet extends Animal {
//...
}
public class Dog extends Pet {
//...
}
又有这样一个方法:
public static void print(Generic extends Animal> a){
System.out.println(a.getData().getName());
}
我们把这个方法和 ? extends X 对应一下,那此时的Animal类就相当于是X,由于它表示类型的上界,所以,我们可以传Animal及其子类。
再画个图来帮助我们理解一下 :
所以如下的操作都是没有问题的:
现在,?extends X 的含义大家应该已经理解,并且知道怎么传递类型参数了吧。
知道了魔咒 “上界不存” 中的 “上界” 的意思了,那 “不存” 又是什么意思呢?
继续往下看:
现在有一个Generic的泛型类,它提供了get和set类型参数变量的方法:
public class Generic {
//...
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
我想通过set方法往里面放数据,但是,set方法却不允许被调用!编译不通过!
这是怎么回事呢???
其实这个主要是出于安全方面考虑。编译器是一个责任心极强的东西,他只做它可以 确定 的事情,不确定的事情它是坚决不做的。
既然 ? extends X 表示类型的上界,类型参数是X的子类,
- 那么可以肯定的是:get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。所以,get它允许调用。
- 但是set方法只知道传入的是个X,至于具体是X的那个子类,编译器不知道。所以,出于安全考虑,编译器干脆就不做了。
所以:? extends X 主要用于安全地访问数据,可以访问X及其子类型,但是不能写入非null的数据。
这就是 “不存” 的意思了,现在,“上界不存” 的咒语你理解了吗?
(2). 再看看 ? super X
它表示传递给方法的参数,必须是X的超类(包括X本身),
与 ? extends X相反,它表示类型下界。
所以下界 就是 针对 ?superX 来说的l了。
我们用类似的方法来理解一下 “下界不取” 的意思。
还是上面的 Animal、Pet 和Dog类。
我们又有一个方法:
public static void printSuper(Generic super Pet> a){
System.out.println(a.getData());
}
那这个方法中的 Pet 就相当于 ? super X 里的X,由于它表示类型的下界,所以,我们可以传Pet及其超类。
同样的,我们也来画个图:
我们再在代码中验证一下:
如此看来,?super X 中 下界 的含义就是:
X说:“我就是底线,不能再比我低了,否则我会生气的,后果很严重!哼!☹”
那 “下界不取” 中的 不取 又是什么道理呢?
我们把之前的代码稍作改变:
我们发现 此时
set方法可以被调用的,且能传入的参数只能是X或者X的子类。
但是get方法只会返回一个Object类型的值,并没有什么实际意义。
对此现象我们 可以这样来理解:
? super X 表示类型的下界,类型参数是X的超类(包括X本身)。
- 那么就可以肯定的说,get方法返回的一定是个X的超类,但是到底是哪个超类?不知道。但是可以肯定的说,Object一定是它的超类,因为Object是所有类的超类,所以get方法返回Object。但是这样取出的全是Object其实并没有什么意义。
- 对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。所以,此时set是被允许的。
所以,? super X 主要用于安全地写入数据,可以写入X及其子类型。但是读取只能得到Object,并没有什么实际意义,所以我们就没有太大必要去get了。
所以 “下界不取” 的 “不取” 含义也就在于此了。
综上,我们就知道 “上界不存,下界不取” 的真实含义其实就是:
都是出于 安全考虑,
- “?extends X” 主要用于安全地访问数据。它只用来读数据,不用来存放数据。
- ? super X 主要用于安全地写入数据。它只用来写入数据,不用来取出数据。
说的官方一点,这里其实就涉及到 PESC 原则了。
即 Producer Extends Consumer Super
如果参数化类型表示一个生产者,就使用 extends T>;如果它表示一个消费者,就使用 super T>即:
只读不可写时,使用Generic extends Animal>:Producer
只写不可读时,使用Generic super Pet>:Consumer
7.泛型使用的局限性
无规矩不成方圆,其实泛型的使用也是有局限性和约束的。
下面就让我们来一一看一下,哪些地方是使用泛型时需要格外注意的~
我们都以 Generic 这个泛型类来讲:
1. 泛型中不能用 基本数据类型 来实例化类型参数,
而要用 基本数据类型对应的包装类才可以。
代码为证:
2. 运行时 只看 原始类型
这个要稍微做个解释,先问大家一个问题 :
(PS:其实这一点也可以从类型擦除角度来理解)
Generic stringG=new Generic<>();
Generic booleanG=new Generic<>();
//输出会是什么?
System.out.println(stringG.getClass()==booleanG.getClass());
//输出又会是什么
System.out.println("stringG.getClass()的name是:"+stringG.getClass().getName());
System.out.println("booleanG.getClass()的name是:"+booleanG.getClass().getName());
答案是:
哇,stringG.getClass() 和 booleanG.getClass() 竟然相等,他们对应的是同一个类型:Generic ,而不是 Generic< String> ,Generic< Boolean>.
这也就说明 运行时的类型 检测,只适用于 原始类型 Generic ,而不是Generic< String> 、Generic< Boolean>.。
所以下面这个代码就是不正确的, instanceof 在 泛型上 是不能使用的,只能用于原始类型。
3. 不能在静态域或方法中引用类型变量
这是为什么呢:
因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行顺序是,先static的部分,然后才是构造函数。
所以在对象初始化之前static的部分就已经执行了,如果你在静态部分引用了泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。
4. 不能实例化类型变量,
也就是:
这个T本来就是虚的,都没有摸清底细,你就乱new 肯定是不行的。
5. 不能捕获泛型类的实例
所以,要注意一点:泛型类是不能extends Exception/Throwable的。
再来:
我们是不能直接捕获泛型类的对象的。那如果我们想捕获 泛型 的异常就没有办法了吗?我们可是伟大的码农,所以方法还是有的:
public void doSth(T x) throws T{
try{
}catch(Throwable e){
throw x;
}
}
虽然不允许我们直接捕获泛型类的对象,但是我们可把 泛型类的对象 throw 出去呀,就是这么机智✌~
7.浅谈类型擦除
最后我们再来提一提泛型中的类型擦除吧。
泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。
由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。
泛型技术在C和Java之中的使用方式看似相同,但实现上却有着根本性的分歧。C里面的泛型是真实泛型,而Java中的泛型其实是伪泛型。
这点怎么说呢?
- 是这样的,在C里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<Integer>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
- 而Java中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<Integer>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
至于为什么java为什么采用 类型擦除 来实现 泛型?
这是因为java 1.5之前并没有泛型这个概念,泛型出现后,为了兼容之前的版本,就采用了类型擦除折中的策略:编译时对泛型要求严格,运行时却把泛型擦除了。
看下面的代码:
编译不通过:
按理说一个方法,我传入的参数类型不一样的话,是可以重载的呀?那这里为什么会呢?
上面这段代码之所以不能编译,是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两种方法的特征签名变得一模一样。
这是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List。所以此处传入的其实是同一个类型。不是重载。
【后记】
以初学者的心态去重学java,我突然发现自己不懂的越来越多了,问号一个接一个的在脑子里跳舞。就拿泛型来说,我还是有很多要去学习和探究的地方,这里也只是做个学习后的整理,等下一次我有了新的收获后再继续分享,希望可以和大家一起,进步进步再进一步~
积累点滴,做好自己~