一、前言                            

还记得JDK1.4时遍历列表的辛酸吗?我可是记忆犹新啊,那时因项目需求我从C#转身到Java的怀抱,然后因JDK1.4少了泛型这样语法糖(还有自动装箱、拆箱),让我受尽苦头啊,不过也反映自己的水平还有待提高,呵呵。JDK1.5引入了泛型、自动装箱拆箱等特性,C#到Java的过渡就流畅了不少。下面我们先重温两者非泛型和泛型的区别吧!

// 非泛型遍历列表
List lst = new ArrayList();
lst.add(1);
lst.add(3);
int sum = 0;
for (Iterator = lst.iterator(); lst.hasNext();){Integer i = (Integer)lst.next();sum += i.intValue();
}// 泛型遍历列表
List<Integer> lst = new ArrayList<Integer>();
lst.add(1);
lst.add(3);
int sum = 0;
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[10];

注意点——父类作为类型参数,则可以子类实例作为集合元素

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("1");
lst.add("2");
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>[10]; // 抛异常
List<?>[] lsa = new ArrayList<?>[10];List<String> list = new ArrayList<String>();
list.add("test");
lsa[0] = list;
System.out.println(lsa[0].get(0));    

四、类型擦除(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 1;}
}

擦除后:

interface Comparable{int compareTo(Object that);
}
final class NumericVal implements Comparable{public int compareTo(NumericVal that){ return 1;}// 编译器自动生成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 = 0;
}
Fruit<Apple>.price = 12;
Fruit<Pear>.price = 5;
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(1, 1L);// 编译成功
Number ret = handle(1, 1L);
Integer ret = handle(1,1);

Number ret = handle(1, 1L)对应的Bytecodes为

14: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: invokevirtual #5                  // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;

而Interger ret = handle(1, 1L)对应的Bytescodes则多了checkcast指令用于作类型转换

14: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: invokevirtual #5                  // Method handle:(Ljava/lang/Number;Ljava/lang/Number;)Ljava/lang/Number;
20: checkcast     #6                  // 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里没有类型转换的语句

3: invokevirtual #2                  // Method java/lang/Class.newInstance:()Ljava/lang/Object;
6: 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/

转载于:https://www.cnblogs.com/fsjohnhuang/p/4288614.html

Java魔法堂:解读基于Type Erasure的泛型相关推荐

  1. java中的类型擦除type erasure

    文章目录 简介 举个例子 原因 解决办法 总结 java中的类型擦除type erasure 简介 泛型是java从JDK 5开始引入的新特性,泛型的引入可以让我们在代码编译的时候就强制检查传入的类型 ...

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

    转载自   Java魔法堂:URI.URL(含URL Protocol Handler)和URN 一.前言 过去一直搞不清什么是URI什么是URL,现在是时候好好弄清楚它们了!本文作为学习笔记,以便日 ...

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

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

  4. java魔法堂_Java魔法堂:调用外部程序

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

  5. Java魔法堂:深入正则表达式API

    目录 一.前言 二.正则表达式的使用诉求 三.java.util.regex包 四.java.lang.String实例 五.最短路径实现诉求 六.Java支持的正则表达式功能语法 七.总结 八.参考 ...

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

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

  7. Java魔法堂:注解用法详解——@SuppressWarnings

    一.前言 编码时我们总会发现如下变量未被使用的警告提示: 上述代码编译通过且可以运行,但每行前面的"感叹号"就严重阻碍了我们判断该行是否设置的断点了.这时我们可以在方法前添加 @S ...

  8. Java魔法堂:初探MessageFormat.format和ChoiceFormat

    一.前言 刚开始从.net的转向java的时候总觉得 String.format 用得不习惯,希望格式模版会这样 {0}, this is {1}'s cat.{1},this is {0}'s do ...

  9. Java魔法堂:注解用法详解——@SuppressWarnings(转)

    一.前言 编码时我们总会发现如下变量未被使用的警告提示: 上述代码编译通过且可以运行,但每行前面的"感叹号"就严重阻碍了我们判断该行是否设置的断点了.这时我们可以在方法前添加 @S ...

最新文章

  1. Leetcode 50. Pow(x, n)
  2. python3.x获取windows自启动程序列表
  3. bootstrap-实现loading效果
  4. linux 命令02
  5. Python-数据结构(二)
  6. 【计算机网络复习 数据链路层】3.5.3 CSMA协议
  7. 我可以做些什么来提高应用程序的可用性?
  8. 计算机cad模板样例,教你如何新建适合自己的CAD模板
  9. pdf编辑器哪个好 怎么添加pdf文件水印
  10. 完整的省市县三级联动
  11. 游戏给你带来了什么,你还在执迷不悟吗?
  12. 【数论】线性筛与积性函数
  13. 看雪学院将举办《安全开发者峰会》,有这11个安全议题
  14. 项目管理100问 | NO.6 如何为项目制定里程碑?
  15. vue适配双端浮标(悬浮球)拖拽
  16. xshell中小键盘乱码的解决方法
  17. 亿级访问量下的新浪微博系统架构
  18. php 自动加载 知乎,wordpress复制文章自动添加版权和原文链接 仿知乎版权功能
  19. 2021考研历程总结
  20. Kubernetes-Ubuntu虚拟机安装kubectl,kubeadm,kubelet

热门文章

  1. 6748如何设置edma为事件触发方式_全面分析前端的网络请求方式
  2. 计算机网络帧及其封装,计算机网络程设计-帧封装.doc
  3. html5 area 获取坐标,HTML 5
  4. MCU——SRAM和Flash
  5. 按键扫描——74HC164驱动(二)
  6. mysql最长字段_在mysql中使用GROUP BY时,如何选择最长的文本字段?
  7. scala apply是什么
  8. nginx官方模块之http_random_index_module
  9. USB杀手上Indigogo众筹 可以毁坏电脑USB接口
  10. 专家:苹果有能力打造Mac芯片但不会去做