Java中泛型区别以及泛型擦除详解
一、引言
复习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);
}
}
Java中泛型区别以及泛型擦除详解的更多相关文章
- java中List的用法和实例详解
java中List的用法和实例详解 List的用法List包括List接口以及List接口的所有实现类.因为List接口实现了Collection接口,所以List接口拥有Collection接口提供 ...
- Java中堆内存和栈内存详解2
Java中堆内存和栈内存详解 Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,ja ...
- Java中的equals和hashCode方法详解
Java中的equals和hashCode方法详解 转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...
- 转:Java中的equals和hashCode方法详解
转自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这 ...
- java中的String类常量池详解
test1: package StringTest; public class test1 { /** * @param args */ public static void main(String[ ...
- java中常见的六种线程池详解
之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如 ...
- Java中堆内存和栈内存详解
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...
- java集合(3)- Java中的equals和hashCode方法详解
参考:http://blog.csdn.net/jiangwei0910410003/article/details/22739953 Java中的equals方法和hashCode方法是Object ...
- JAVA中GridBagLayout布局管理器应用详解
很多情况下,我们已经不需要通过编写代码来实现一个应用程序的图形界面,而是通过强大的IDE工具通过拖拽辅以简单的事件处理代码即可很轻松的完成.但是我们不得不面对这样操作存在的一些问题,有时候我们希望能够 ...
- Java中的queue和deque对比详解
队列(queue)简述 队列(queue)是一种常用的数据结构,可以将队列看做是一种特殊的线性表,该结构遵循的先进先出原则.Java中,LinkedList实现了Queue接口,因为LinkedLis ...
随机推荐
- 最舒适的路(并查集+枚举)(hdu1598)
hdu1598 find the most comfortable road Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768 ...
- 为什么不要使用"using namespace XXX"
为什么不要使用"using namespace XXX" 1.避免降低性能 2.避免Entity冲突 This is not related to performance at a ...
- 【Maven学习】Nexus私服代理其他第三方的Maven仓库
一.背景 [Maven学习]Nexus OSS私服仓库的安装和配置 http://blog.csdn.net/ouyang_peng/article/details/78793038 [Maven学习 ...
- 【抓包】火狐浏览器F12
页面请求服务器的get.post两种请求,还有其他种,但是其他中基本不用,所以只记住get和post两种请求方法即可. 1.get(当前页面向服务器传值--即请求服务器)---弊端--传值长度有限 F ...
- Python之迭代器及生成器
一. 迭代器 1.1 什么是可迭代对象 字符串.列表.元组.字典.集合 都可以被for循环,说明他们都是可迭代的. 我们怎么来证明这一点呢? from collections import Itera ...
- Linux下编译安装Nginx1.12
[准备工作] 所有操作需要在root用户下 本机测试案例系统信息:centos7.3 安装路径:/usr/local/nginx [安装Nginx] 先安装如下依赖包 $ yum install gc ...
- Spark的RDD原理以及2.0特性的介绍
转载自:http://www.tuicool.com/articles/7VNfyif 王联辉,曾在腾讯,Intel 等公司从事大数据相关的工作.2013 年 - 2016 年先后负责腾讯 Yarn ...
- HDU1695:GCD(容斥原理+欧拉函数+质因数分解)好题
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1695 题目解析: Given 5 integers: a, b, c, d, k, you're to ...
- grub的安装与配置-------引导redhat grub
1.安装 有两种方法: a.在联网的情况下,用新立德安装: apt-get install grub b.在没网的时候,特别是linux网卡驱动没有安装: 自己从http://packages.ubu ...
- C++中的常量定义
本篇笔记总结自一次代码检视. 一般来说,使用C语言编程时我们都习惯在代码当中使用C当中的宏定义来定义一个数值常量: #define MY_CONST 7 在C++开发项目时,也会经常存在沿袭C当中常量 ...