Java魔法堂:解读基于Type Erasure的泛型

简介:

一、前言                            

  还记得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/

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!

分类: Java
0
0
« 上一篇: 代数几何:三角函数
» 下一篇: JS魔法堂: Native Promise Only源码剖析
posted @ 2015-02-13 16:03 ^_^肥仔John 阅读( 845) 评论( 0) 编辑 收藏
 
相关文章
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
25 0
[Java]泛型
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
14 1
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
27 5
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
20 2
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
16 0
|
2月前
|
Java
【Java】什么是泛型?什么是包装类
【Java】什么是泛型?什么是包装类
18 0
|
6月前
|
Java API 容器
Java泛型的继承和通配符
Java泛型的继承和通配符
38 1
|
7月前
|
安全 Java API
Java一分钟之-泛型通配符:上限与下限野蛮类型
【5月更文挑战第19天】Java中的泛型通配符用于增强方法参数和变量的灵活性。通配符上限`? extends T`允许读取`T`或其子类型的列表,而通配符下限`? super T`允许向`T`或其父类型的列表写入。野蛮类型不指定泛型,可能引发运行时异常。注意,不能创建泛型通配符实例,也无法同时指定上下限。理解和适度使用这些概念能提升代码的通用性和安全性,但也需兼顾可读性。
67 3
|
7月前
|
Java 编译器
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符