一、前言                            

还记得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的泛型的更多相关文章

  1. Java魔法堂:类加载器入了个门

    一.前言 <Java魔法堂:类加载机制入了个门>中提及整个类加载流程中只有加载阶段作为码农的我们可以入手干预,其余均由JVM处理.本文将记录加载阶段的核心组件——类加载器的相关信息,以便日 ...

  2. Java魔法堂:打包知识点之jar

    一.前言    通过eclipse导出jar包十分方便快捷,但作为码农岂能满足GUI的便捷呢?所以一起来CLI吧! 二.JAR包 JAR包是基于ZIP文件格式,用于将多个.java文件和各种资源文件, ...

  3. 【转】Java魔法堂:String.format详解

    Java魔法堂:String.format详解     目录     一.前言    二.重载方法     三.占位符     四.对字符.字符串进行格式化     五.对整数进行格式化     六. ...

  4. Java魔法堂:URI、URL(含URL Protocol Handler)和URN

    一.前言 过去一直搞不清什么是URI什么是URL,现在是时候好好弄清楚它们了!本文作为学习笔记,以便日后查询,若有纰漏请大家指正! 二.从URI说起    1. 概念 URI(Uniform Reso ...

  5. Java魔法堂:类加载机制入了个门

    一.前言 当在CMD/SHELL中输入 $ java Main<CR><LF> 后,Main程序就开始运行了,但在运行之前总得先把Main.class及其所依赖的类加载到JVM ...

  6. Java魔法堂:Date与日期时间格式化

    一.前言                                                                                       日期时间的获取.显 ...

  7. Java魔法堂:调用外部程序

    前言 Java虽然五脏俱全但总有软肋,譬如获取CPU等硬件信息,当然我们可以通过JNI调用C/C++来获取,但对于对C/C++和Windows API不熟的码农是一系列复杂的学习和踩坑过程.那能不能通 ...

  8. Java魔法堂:枚举类型详解

    一.前言 Java的枚举类型相对C#来说具有更灵活可配置性,Java的枚举类型可以携带更多的信息. // C# enum MyColor{ RED = , BLUE = } Console.Write ...

  9. Java魔法堂:JVM的运行模式

    一.前言 JVM有Client和Server两种运行模式.不同的模式对应不同的应用场景,而JVM也会有相应的优化.本文将记录JVM模式的信息,以便日后查阅. 二.介绍 在$JAVA_HOME/jre/ ...

随机推荐

  1. android precelable和Serialization序列化数据传输

    一 序列化原因: 1.永久性保存对象,保存对象的字节序列到本地文件中:2.通过序列化对象在网络中传递对象:3.通过序列化在进程间传递对象. 二 至于选取哪种可参考下面的原则: 1.在使用内存的时候,P ...

  2. AlwaysOn 同步时间的测试

    背景 <SQL Server 2012实施与管理实战指南>中指AlwaysON同步过程如下: 任何一个SQL Server里都有个叫Log Writer的线程,当任何一个SQL用户提交一个 ...

  3. 每周一书-《Bootstrap基础教程》

    首先说明,本周活动有效时间为8月15日到21日.本周为大家送出的书是有电子工业出版,贺臣/陈鹏编著的<Bootsrap基础教程>,为前端入门必读书籍. 下面是从书中摘录的内容. “ Boo ...

  4. 一个App完成入门篇(二)-搭建主框架

    通过第一课的学习,你已经掌握了如何通过debug调试器来跟PC上的设计器联调来实时查看UI设计效果.调试代码了,接下来通过一系列的demo开发教学你将很快上手学习到如何开发一个真正的App. 要开发A ...

  5. js模版引擎handlebars.js实用教程——结束语

    返回目录 有了这些功能,[ajax+json+Handlebars]替代[vo+el表达式]不成问题,新时代的曙光已经来临,最佳解决方案在此,您还等什么? 教程到此结束...祝读者学习愉快... 小菜 ...

  6. from表单iframe原网页嵌入

    今天是巩固的from表单跟嵌入其他页面,同样的,学习到了新的知识. 温故而知新: iframe--在原页面嵌入其他页面,以窗口的样式 其中scrolling--滚动条 noresize--可调整大小 ...

  7. salesforce 零基础学习(三十四)动态的Custom Label

    custom label在项目中经常用到,常用在apex class或者VF里面用来显示help text或者error message.有的时候我们需要用到的信息是动态变化的,那样就需要动态来显示信 ...

  8. iOS----CocoaPods的安装、使用和,原理+参考流程+常见问题

    一.什么是CocoaPods CocoaPods是iOS项目的依赖管理工具,该项目源码在Github上管理.开发iOS项目不可避免地要使用第三方开源库,CocoaPods的出现使得我们可以节省设置和第 ...

  9. Android Activity 常用技巧

    1.设置 Activity 背景色为透明 在style.xml里面声明: <style name="TranslucentActivityStyle" parent=&quo ...

  10. artTemplate 这么叼

    artTemplate 这么叼 高性能JavaScript模板引擎原理解析 http://www.itfeed.com/blog/10016.html