一、引言
复习javac的编译过程中的解语法糖的时候看见了泛型擦除中的举例,网上的资料大多比较散各针对性不一,在此做出自己的一些详细且易懂的总结。
二、泛型简介
泛型是JDK 1.5的一项新特性,一种编译器使用的范式,语法糖的一种,能保证类型安全。【注意:继承中,子类泛型数必须不少于父类泛型数】
为了方便理解,我将泛型分为普通泛型和通配泛型
三、泛型分类
1、普通泛型
就是没有设置通配的泛型,泛型表示为某一个类。
声明时: class Test<T>{...}
使用时: Test<Integer> test = new Test<Integer>();
作为无界泛型,其实就是约束左右泛型必须一致。
2、通配泛型
通配泛型包括两种,无界通配和有界通配。
【无界通配符】
<?>通配符——表示所有类型都能与它匹配
【有界通配符】
extends(上界)通配符——声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类;
super(下界)通配符——声明了类型的下界,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。类型擦除后剩下(下面以extends举例)
//有界泛型类型语法 - 继承自某父类 <T extends ClassA> //有界泛型类型语法 - 实现某接口 <T extends InterfaceB> //有界泛型类型语法 - 多重边界 <T extends ClassA & InterfaceB & InterfaceC ... > //示例 <N extends Number> //N标识一个泛型类型,其类型只能是Number抽象类的子类 <T extends Number & Comparable & Map> //T标识一个泛型类型,其类型只能是Person类型的子类,并且实现了Comparable 和Map接口
【注意:多重边界里,只允许第一个能为类,后续必须为接口】
四、List<T> 和 List<?> 和 List<Object> 的区别
类声明的时候采用List<T>,此时T可以为任何字母,都指代普通泛型。
例如: class Test<T>{...}
实例化的时候采用List<?>,此时?可以为任何类,表示只能存入此类对象,也可以就写‘<?>’,代表可以存入任何类对象,属于通配泛型。
例如:List<?> listOfString = new ArrayList<String>;
但是注意List<?>与List<Object>不一样,前者是所有泛型的通配符,即所有泛型的引用都能与他进行匹配(作为实例化的右边),而Object只是一个单独的类,当为实例化左边的时候,有且仅有为<Object>相匹配(或者不写泛型),例如:List<Object> list = new ArrayList<Object>();,实例化左边的泛型作为“答案范围”,实例化右边的泛型只能为“答案”是某一个类。例如:
List<String> list = new ArrayList<?>(); // 编译错误:通配符是“答案范围”不能作为“答案”出现在实例化的右边 List<?> list = new ArrayList<String>(); // String与?匹配成功 List<? extends Number> list = new ArrayList<? extends Integer>(); // 编译错误:有界泛型同样也是“答案范围”,不能出现在实例化的右边 List<? extends Number> list = new ArrayList<Integer>(); // 右边的"答案"与左边的“答案范围”匹配成功
五、泛型擦除
由来:一开始java并没有泛型,后来1.5加入了泛型,为了能向前兼容(旧版本的jvm能解释运行新版本的.class文件)所以就采用了伪泛型——“泛型擦除”,并一直保留了下来。
原理:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,擦除后会变成原始类型(去掉<T>,将方法内的T擦除成Object)例如Generic<T>会被擦除成Generic。还需要注意的是,不同的通配符的擦除的方式也有不同:
口诀:【存入:取下界;取出:取上界】—or—【存下,取上】
当泛型作为方法的传入参数的时候,此时替换成通配泛型的下界,例如add方法
当泛型作为方法的返回参数的时候,此时替换成通配泛型的上界,例如get方法
List<? extends Integer> list1 = new ArrayList<Integer>(); list1.add(null); // 此时传入取<? extends Integer> 下界————无 所以只能传null,否则报错 Integer integer1 = list1.get(0); // // 此时返回取<? extends Integer> 上界————Integer List<? super Integer> list2 = new ArrayList<Integer>(); list2.add(111); // 此时传入取<? super Integer> 下界——————Integer Integer integer2 = (Integer) list2.get(0); // // 此时返回取<? super Integer> 上界————Object
所以同理可得,当泛型为<?>的时候,取下界是null,取上界是Object。
所以得出结论,因为add和get方法的擦除的限制,尽量少使用通配泛型
泛型擦除有什么隐患,有什么解决方法:
1、如果不加泛型继承,擦除后会变成原始类型,所以能加入非泛型的类型。 List<Integer> list = new ArrayList<Integer>(); list.add("呀哈");
并且能完成不同泛型之间的引用传递。 List<String> list = new ArrayList<Integer>();
以上两种情况怎么解决?
——java编译器是通过先检查代码中泛型的类型,如果出现上面两种情况则会在编译期报错,检查通过后,然后再进行类型擦除,再进行编译。
【注意:先检查实例化左右泛型是否匹配,然后以实例化的左边的泛型为基准对添加元素进行检查(所以,只在右边写泛型和没写是一个意思)】例如:
List<String> list1 = new ArrayList<String>(); // 此时按照String检查泛型 List list2 = new ArrayList<String>(); // 此时不检查泛型
2、擦除后泛型信息就没了,获取的时候再强转?
——泛型补偿:在泛型检查的保证下,存入的都是符合泛型的对象,编译期间利用反射获取元素对象的类型(getClass()方法)对要传出元素进行强转。
3、子类继承泛型方法,然后对其重写并将泛型改成真实类型,但是在擦除之后原来父类的泛型方法会变成Object方法,变为两个不同的方法,这样一来此方法就不是继承重写,而是子类的重载了。如下面代码所示:
class Node<T> { public void setData(T data) { System.out.println("Node.setData"); } } class MyNode<T> extends Node<Integer> { public void setData(Integer data) { System.out.println("MyNode.setData:"+data); } }
Node<Integer> n = new MyNode<Integer>(); n.setData(1213); // 如果是擦除后的Object方法则会执行父类的方法,打印出“Node.setData”
运行结果: MyNode.setData:1213
可见执行的却是子类的方法?完成了多态的实现。这是怎么解决的?
——桥方法:顾名思义,因为擦除之后子类中方法的参数列表与父类参数列表不同,不能形成重写,所以编译器在编译的时候,擦除后往子类中插入一些方法用来重载父类中的所有泛型擦除之后的Object方法,并在方法内部调用相对应的子类方法,以此重新形成父子之间多态,这些方法被称为桥方法。(下面是编译器擦除编译之后的内容)
class Node { public void setData(Object data) { System.out.println("Node.setData"); } } class MyNode extends Node { // 编译器生成的桥方法 public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData:"+data); } }