Java魔法堂:解读基于Type Erasure的泛型
一、前言
还记得JDK1.4时遍历列表的辛酸吗?我可是记忆犹新啊,那时因项目需求我从C#转身到Java的怀抱,然后因JDK1.4少了泛型这样语法糖(还有自动装箱、拆箱),让我受尽苦头啊,不过也反映自己的水平还有待提高,呵呵。JDK1.5引入了泛型、自动装箱拆箱等特性,C#到Java的过渡就流畅了不少。下面我们先重温两者非泛型和泛型的区别吧!
// 非泛型遍历列表
List lst = new ArrayList();
lst.add();
lst.add();
int sum = ;
for (Iterator = lst.iterator(); lst.hasNext();){
Integer i = (Integer)lst.next();
sum += i.intValue();
} // 泛型遍历列表
List<Integer> lst = new ArrayList<Integer>();
lst.add();
lst.add();
int sum = ;
for (Iterator = lst.iterator(); lst.hasNext();){
Integer i = lst.next();
sum += i;
}
泛型的最主要作用是在编译时期就检查集合元素的类型,而不是运行时才抛出ClassCastException。
泛型的官方文档:http://docs.oracle.com/javase/tutorial/java/generics/erasure.html
注意:以下内容基于JDK7和HotSpot。
二、认识泛型
在介绍之前先定义两个测试类,分别是 类P 和 类S extends P 。
1. 声明泛型变量,如 List<String> lst = new ArrayList<String>();
注意点——泛型不支持协变
// S为P的子类,但List<S>并不是List<P>的子类,也就是不支持协变
// 因此下列语句无法通过编译
List<P> lst = new ArrayList<S>(); // 而数组支持协变
P[] array = new S[];
注意点——父类作为类型参数,则可以子类实例作为集合元素
List<P> lst = new ArrayList<P>();
lst.add(new S());
2. 声明带通配符泛型变量,如 List<?> lst = new ArrayList<P>();
通配符 ? 表示类型参数为未知类型,因此可赋予任何类型的类型参数给它。
当集合的类型参数 ? 为时,无法向集合添加除null外的其他类型的实例。(null属于所有类的子类,因此可以赋予到未知类型中)
List<?> lst = new ArrayList<P>();
lst = new ArrayList<S>();
// 以下这句将导致编译失败
lst.add(new S()); // 以下这句则OK
lst.add(null);
因此带通配符的泛型变量一般用于检索遍历集合元素使用,而不做添加元素的操作。
void read(List<?> lst){
for (Object o : lst){
System.out.println((o.toString());
}
}
List<String> lst = new ArrayList<String>();
lst.add("");
lst.add("");
read(lst);
到这里会发现使用带通配符的泛型集合(unbounded wildcard generic type) 与 使用非泛型集合(raw type)的效果是一样的,其实并不是这样.
我们可以向非泛型集合添加任何类型的元素, 而通配符的泛型集合则只允许添加null而已, 从而提高了类型安全性. 而且我们还可以使用带限制条件的带边界通配符的泛型集合呢!
3. 声明带边界通配符 ? extends 的泛型变量,如 List<? extends P> lst = new ArrayList<S>();
边界通配符 ? extends 限制了实际的类型参数必须为指定的类本身或其子类才能通过编译。
void read(List<? extends P> lst){
for (P p : lst){
System.out.println(p);
}
}
List<P> lst = new ArrayList<P>();
lst.add(new P());
lst.add(new S());
read(lst);
4. 声明带边界通配符 ? super 的泛型变量,如 List<? super S> lst = new ArrayList<P>();
边界通配符 ? super限制了实际的类型参数必须为指定的类本身或其父类才能通过编译。
注意:集合元素的类型必须为指定的类本身或其子类。
void read(List<? super S> lst){
for (S s : lst)
System.out.println(s);
}
List<P> lst = new ArrayList<P>();
lst.add(new S());
read(lst);
5. 定义泛型类或接口,如 class Fruit<T>{} 和 interface Fruit<T>{}
T为类型参数占位符,一般以单个大写字母来命名。以下为推荐的占位符名称:
K——键,比如映射的键。
V——值,比如List、Set的内容,Map中的值
E——异常类
T——泛型
除了异常类、枚举和匿名内部类外,其他类或接口均可定义为泛型类。
泛型类的类型参数可供实例方法、实例字段和构造函数中使用,不能用于类方法、类字段和静态代码块上。
class Fruit<T>{
// 类型参数占位符作为实例字段的类型
private T fruit; // 类型参数占位符作为实例方法的返回值类型
T getFruit(){
return fruit;
}
// 类型参数占位符作为实例方法的入参类型
void setFruit(T fruit){
this.fruit = fruit;
}
private List<T> fruits;
// 类型参数占位符作为边界通配符的限制条件
void setFruits(List<? extends T> lst){
fruits = (List<T>)lst;
}
// 类型参数占位符作为实例方法的入参类型的类型参数
void setFruits2(List<T> lst){
fruits = lst;
} // 构造函数不用带泛型
Fruit(){
// 类型参数占位符作为局部变量的类型
fruits = new ArrayList<T>();
T fruit = null;
}
}
和边界通配符一般类型参数占位符也可带边界,如 class Fruit<T extends P>{} 。当有多个与关系的限制条件时,则用&来连接多个父类,如 class Fruit<T extends A&B&C&D>{} 。
也可以定义多个类型参数占位符,如 class Fruit<S,T>{} 、 class Fruit<S, T extends A>{} 等。
下面到关于继承泛型类或接口的问题了,假设现在有泛型类P的类定义为 class P<T>{} ,那么在继承类P时我们有两种选择
1. 指定类P的类型参数
2. 继承类P的类型参数
// 1. 指定父类的类型参数
class S extends P<String>{} // 2. 继承父类的类型参数
class S<T> extends P<T>{}
6.使用泛型类或接口,如 Fruit<?> fruit = new Fruit<Apple>();
现在问题来了,假如Fruit类定义如下: public class Fruit<T extends P>{}
那么假设使用方式为 Fruit<? extends String> fruit; ,大家决定编译能通过吗?答案是否定的,类型参数已经被限制为P或P的子类了,因此只有 Fruit<? extends P> 或 Fruit<? extends S> 可通过编译。
7. 定义泛型方法
无论是实例方法、类方法还是抽象方法均可以定义为泛型方法。
// 实例方法
public <T> void say(T[] msgs){
for (T msg : msgs)
System.out.println(msg.toString());
}
public <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{
return clazz.newInstance();
} // 类方法
public static <T> void say(T msg){
System.out.println(msg.toString());
}
public static <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{
return clazz.newInstance();
} // 抽象方法
public abstract <T> void say(T msg);
public abstract <T extends P> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException{}
8. 使用泛型方法
使用泛型方法分别有 隐式指定实际类型 和 显式指定实际类型 两种形式。
P p = new P();
String msg = "Hello";
// 隐式指定实际类型
p.say(msg); // 显式指定实际类型
p.<String>say(msg);
一般情况下使用隐式指定实际类型的方式即可。
9. 使用泛型数组
只能使用通配符来创建泛型数组
List<?>[] lsa = new ArrayList<String>[]; // 抛异常
List<?>[] lsa = new ArrayList<?>[]; List<String> list = new ArrayList<String>();
list.add("test");
lsa[] = list;
System.out.println(lsa[].get());
四、类型擦除(Type Erasure)和代码膨胀(Code Bloat)
到此大家对Java的泛型有了一定程度的了解了,但在应用时却时不时就发生些匪夷所思的事情。在介绍这些诡异案例之前,我们要补补一些基础知识,那就是Java到底是如何实现泛型的。
泛型的实现思路有两种
1. Code Specialization:在实例化一个泛型类或泛型方法时将产生一份新的目标代码(字节码或二进制码)。如针对一个泛型List,当程序中出现List<String>和List<Integer>时,则会生成List<String>,List<Integer>等的Class实例。
2. Code Sharing:对每个泛型只生成唯一一份目标代码,该泛型类的所有实例的数据类型均映射到这份目标代码中,在需要的时候执行类型检查和类型转换。如针对List<String>和List<Integer>只生成一个List<Object>的Class实例。
C++的模板 和 C# 就是典型的Code Specialization。由于在程序中出现N种L泛型List则会生成N个Class实例,因此会造成代码膨胀(Code Bloat)。
而Java则采用Code Sharing的思路,并通过类型擦除(Type Erasure)来实现。
类型擦除的过程大致分为两步:
①. 使用泛型参数extends的边界类型来代替泛型参数(<T> 默认为<T extends Object>,<?>默认为<? extends Object>)。
②. 在需要的位置插入类型检查和类型转换的语句。
interface Comparable<T>{
int compareTo(T that);
}
final class NumericVal implements Comparable<NumericVal>{
public int compareTo(NumericVal that){ return ;}
}
擦除后:
interface Comparable{
int compareTo(Object that);
}
final class NumericVal implements Comparable{
public int compareTo(NumericVal that){ return ;}
// 编译器自动生成
public int compareTo(Object that){
return this.compareTo((NumbericVal)that);
}
}
也就是说
List<String> lstStr = new ArrayList<String>();
List<Integer> intStr = new ArrayList<Integer>();
System.out.println(lstStr.getClass() == intStr.getClas()); // 显示true,因为lstStr和intStr的类型均被擦除为List了
五、各种基于Type Erasure的泛型的诡异场景
1. 泛型类型共享类变量
class Fruit<T>{
static String price = ;
}
Fruit<Apple>.price = ;
Fruit<Pear>.price = ;
System.out.println(Fruit.<Apple>.price); // 输出5
2. instanceof 类型参数占位符 抛出编译异常
List<String> strLst = new ArrayList<String>();
if (strLst instanceof List<String>){} // 不通过编译
if (strLst instanceof List){} // 通过编译
3. new 类型参数占位符 抛出编译异常
class P<T>{
T val = new T(); // 不通过编译
}
4. 定义泛型异常类 抛出编译异常
class MyException<T> extends Exception{} // 不通过编译
5. 不同的泛型类型形参无法作为不同描述符标识来区分方法
// 视为相同的方法,因此会出现冲突
public void say(List<String> msg){}
public void say(List<Integer> number){} // JDK6后可通过不同的返回值类来解决冲突
// 对于Java语言而言,方法的签名仅为方法名+参数列表,但对于Bytecodes而言方法的签名还包含返回值类型。因此在这种特殊情况下,Java编译器允许这种处理手段
public void say(List<String> msg){}
public int say(List<Integer> number){}
六、再深入一些
1. 采用隐式指定类型参数类型的方式调用泛型方法,那到底是如何决定的实际类型呢?
假如现有一个泛型方法的定义为 <T extends Number> T handle(T arg1, T arg2){ return arg1;}
那么根据类型擦除的操作步骤,T的实际类型必须是Number的。看看字节码吧 Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;Ljava/lang/Number;
剩下的就是类型检查和类型转换的活了,根据不同的入参类型和对返回值进行类型转换的组合将导致不同的结果。
// 编译时报“交叉类型”编译失败
Integer ret = handle(, 1L); // 编译成功
Number ret = handle(, 1L);
Integer ret = handle(,);
Number ret = handle(1, 1L)对应的Bytecodes为
: invokestatic # // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
: invokevirtual # // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;
而Interger ret = handle(1, 1L)对应的Bytescodes则多了checkcast指令用于作类型转换
: invokestatic # // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
: invokevirtual # // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;
: checkcast # // class java/lang/Integer
根据上述规则,所以下列代码会由于方法定义冲突而编译失败
// 编译失败
<T extends String> void println(T msg){}
void println(String msg){}
2. 效果一致但写法不同的两个泛型方法
public static <T extends P> T getP1(Class<T> clazz){
T ret = null;
try{
ret = clazz.newInstance();
}
catch(InstantiationException|IllegalAccessException e){}
return ret;
}
} public static <T> T getP2(Class<? extends P> clazz){
T ret = null;
try{
ret = (T)clazz.newInstance();
}
catch(InstantiationException|IllegalAccessException e){}
return ret;
}
}
getP1的内容不难理解,类型参数占位符T会被编译成P,因此类型擦除后的代码为:
public static P getP1(Class clazz){
P ret = null;
try{
ret = (P)clazz.newInstance();
}
catch(InstantiationException|IllegalAccessException e){}
return ret;
}
}
而getP2中T被编译为Object,而clazz.newInstance()返回值类型为Object,那么为什么要加(T)来进行显式的类型转换呢?但假如将<T>改成<T extends Number>,那显式类型转换就变为必须品了。我猜想是因为getP2的书写方式导致返回值与入参的两者的类型参数是没有任何关联的,无法保证一定能成功地执行隐式类型转换,因此规定开发人员必须进行显式的类型转换,否则就无法通过编译。但最吊的是Bytecodes里没有类型转换的语句
: invokevirtual # // Method java/lang/Class.newInstance:()Ljava/lang/Object;
: astore_1
七、总结
若有纰漏请大家指正,谢谢!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4288614.html ^_^肥仔John
八、参考
http://blog.zhaojie.me/2010/02/why-not-csharp-on-jvm-type-erasure.html
http://blog.csdn.net/lonelyroamer/article/details/7868820
http://www.programcreek.com/2013/12/raw-type-set-vs-unbounded-wildcard-set/
Java魔法堂:解读基于Type Erasure的泛型的更多相关文章
- Java魔法堂:类加载器入了个门
一.前言 <Java魔法堂:类加载机制入了个门>中提及整个类加载流程中只有加载阶段作为码农的我们可以入手干预,其余均由JVM处理.本文将记录加载阶段的核心组件——类加载器的相关信息,以便日 ...
- Java魔法堂:打包知识点之jar
一.前言 通过eclipse导出jar包十分方便快捷,但作为码农岂能满足GUI的便捷呢?所以一起来CLI吧! 二.JAR包 JAR包是基于ZIP文件格式,用于将多个.java文件和各种资源文件, ...
- 【转】Java魔法堂:String.format详解
Java魔法堂:String.format详解 目录 一.前言 二.重载方法 三.占位符 四.对字符.字符串进行格式化 五.对整数进行格式化 六. ...
- Java魔法堂:URI、URL(含URL Protocol Handler)和URN
一.前言 过去一直搞不清什么是URI什么是URL,现在是时候好好弄清楚它们了!本文作为学习笔记,以便日后查询,若有纰漏请大家指正! 二.从URI说起 1. 概念 URI(Uniform Reso ...
- Java魔法堂:类加载机制入了个门
一.前言 当在CMD/SHELL中输入 $ java Main<CR><LF> 后,Main程序就开始运行了,但在运行之前总得先把Main.class及其所依赖的类加载到JVM ...
- Java魔法堂:Date与日期时间格式化
一.前言 日期时间的获取.显 ...
- Java魔法堂:调用外部程序
前言 Java虽然五脏俱全但总有软肋,譬如获取CPU等硬件信息,当然我们可以通过JNI调用C/C++来获取,但对于对C/C++和Windows API不熟的码农是一系列复杂的学习和踩坑过程.那能不能通 ...
- Java魔法堂:枚举类型详解
一.前言 Java的枚举类型相对C#来说具有更灵活可配置性,Java的枚举类型可以携带更多的信息. // C# enum MyColor{ RED = , BLUE = } Console.Write ...
- Java魔法堂:JVM的运行模式
一.前言 JVM有Client和Server两种运行模式.不同的模式对应不同的应用场景,而JVM也会有相应的优化.本文将记录JVM模式的信息,以便日后查阅. 二.介绍 在$JAVA_HOME/jre/ ...
随机推荐
- 设计模式之美:Mediator(中介者)
索引 意图 结构 参与者 适用性 效果 相关模式 实现 实现方式(一):Mediator 模式结构样式代码. 意图 用一个中介对象来封装一系列的对象交互. 中介者使各对象不需要显式地相互引用,从而使其 ...
- 作业七:团队项目——Alpha版本冲刺阶段-08
昨天进展:代码编写. 今天安排:代码编写.
- web前端职业规划(转)
关于一个WEB前端的职业规划,其实是有各种的答案,没有哪种答案是完全正确的,全凭自己的选择,只要是自己选定了, 坚持去认真走,就好.在这里,我只是简要说一下自己对于这块儿内容的理解.有一个观点想要分享 ...
- FusionCharts简单教程(四)-----基本数字格式
在统计图例中什么是最基本,最重要的元素?那就是数据.一个数据的统计图像那就是一堆空白.但是数据存在多种形式,比如小数,比如千分位等等.又如若一个数据是12.000000001,对于数据要求 ...
- AWS re:Invent 2014回顾
亚马逊在2014年11月11-14日的拉斯维加斯举行了一年一度的re:Invent大会.在今年的大会上,亚马逊一股脑发布和更新了很多服务.现在就由我来带领大家了解一下这些新服务. 安全及规范相关 AW ...
- Atitit.软件中见算法 程序设计五大种类算法
Atitit.软件中见算法 程序设计五大种类算法 1. 算法的定义1 2. 算法的复杂度1 2.1. Algo cate2 3. 分治法2 4. 动态规划法2 5. 贪心算法3 6. 回溯法3 7. ...
- salesforce 零基础开发入门学习(八)数据分页简单制作
本篇介绍通过使用VF自带标签和Apex实现简单的数据翻页功能. 代码上来之前首先简单介绍一下本篇用到的主要知识: 1.ApexPages命名空间 此命名空间下的类用于VF的控制. 主要的类包括但不限于 ...
- MongoDB 简介
MongoDB 简介 介绍:MongoDB是一个基于分布式文件存储的数据库.由C++语言编写.旨在为WEB应用提供可扩展的高性能数据存储解决方案.特点:高性能.易部署.易使用,存储数据非常方便.主要功 ...
- JS checkbox 全选 全不选
/* JS checkbox 全选 全不选 Html中checkbox: <input type="checkbox" name="cbx" value= ...
- Java EE开发平台随手记2——Mybatis扩展1
今天来记录一下对Mybatis的扩展,版本是3.3.0,是和Spring集成使用,mybatis-spring集成包的版本是1.2.3,如果使用maven,如下配置: <properties&g ...