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