一:Java泛型的实现方法:类型擦除

  大家都知道,Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

  如在代码中定义List<Object> 和List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与C++模板机制实现方式之间的重要区别。

  通过两个例子证明Java类型的类型擦除:

  例1:

  1. public class Test4 {
  2. public static void main(String[] args) {
  3. ArrayList<String> arrayList1=new ArrayList<String>();
  4. arrayList1.add("abc");
  5. ArrayList<Integer> arrayList2=new ArrayList<Integer>();
  6. arrayList2.add(123);
  7. System.out.println(arrayList1.getClass()==arrayList2.getClass());
  8. }
  9. }

  在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String> 泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过 arrayList1对象和arrayList2对象的getClass() 方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

  例2:

  1. public class Test4 {
  2. public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  3. ArrayList<Integer> arrayList3=new ArrayList<Integer>();
  4. arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
  5. arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
  6. for (int i=0;i<arrayList3.size();i++) {
  7. System.out.println(arrayList3.get(i));
  8. }
  9. }

  在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

  

二:类型擦除后保留的原始类型

  在上面,两次提到了原始类型,什么是原始类型?原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

  例3:

  1. class Pair<T> {
  2. private T value;
  3. public T getValue() {
  4. return value;
  5. }
  6. public void setValue(T value) {
  7. this.value = value;
  8. }
  9. }

  Pair<T>的原始类型为:

  1. class Pair {
  2. private Object value;
  3. public Object getValue() {
  4. return value;
  5. }
  6. public void setValue(Object value) {
  7. this.value = value;
  8. }
  9. }

  因为在 Pair<T>中, T是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair, 如Pair<String> OR Pair<Integer>,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object。

  从上面的例2中,我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变为Object, 所以通过反射我们就可以存储字符串了。

  如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。

  比如: Pair这样生命的话

  1. public class Pair<T extends Comparable> {

  那么原始类型就是Comparable。

  要区分原始类型和泛型变量的类型

  在调用泛型方法时,可以指定泛型,也可以不指定泛型。

    ①. 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,知道Object。

    ②. 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类。

  1. public class Test2{
  2. public static void main(String[] args) {
  3. /**不指定泛型的时候*/
  4. int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
  5. Number f=Test2.add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
  6. Object o=Test2.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
  7.  
  8. /**指定泛型的时候*/
  9. int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
  10. int b=Test2.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float
  11. Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
  12. }
  13.  
  14. //这是一个简单的泛型方法
  15. public static <T> T add(T x,T y){
  16. return y;
  17. }
  18. }

  其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList可以存储任意的对象。

  举例:

  1. public static void main(String[] args) {
  2. ArrayList arrayList=new ArrayList();
  3. arrayList.add(1);
  4. arrayList.add("121");
  5. arrayList.add(new Date()); }

三:类型擦除引起的问题及解决方法

  因为种种原因,Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀问题,但是也引起来许多新问题,所以,SUN对这些问题做出了种种限制,避免我们发生各种错误。

  1. 先检查, 再编译以及编译的对象和引用传递问题

    既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList<String> 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?Java如何解决这个问题的呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

    例如:

  1. public static void main(String[] args) {
  2. ArrayList<String> arrayList=new ArrayList<String>();
  3. arrayList.add("123");
  4. arrayList.add(123);//编译错误
  5. }

    在上面的程序中,使用 add 方法添加一个整形,在Eclipse中,直接回报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

    那么,这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容。

    以 ArrayList举例子,以前的写法:

  1. ArrayList arrayList=new ArrayList();

    现在的写法:

  1. ArrayList<String> arrayList=new ArrayList<String>();

    如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:

  1. ArrayList<String> arrayList1=new ArrayList(); //第一种 情况
  2. ArrayList arrayList2=new ArrayList<String>();//第二种 情况

    这样是没有错误的,不过会有个编译时警告。

    不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。

    因为类型检查就是编译时完成的, new ArrayList() 只是在内存中开辟了一个存储空间, 可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用 arrayList1 来调用它的方法,比如说调用 add方法,所以arrayList1引用能完成泛型类型的检查。而引用arrayList2没有使用泛型,所以不行。

    举例子:

  1. public class Test10 {
  2. public static void main(String[] args) {
  3.  
  4. //
  5. ArrayList<String> arrayList1=new ArrayList();
  6. arrayList1.add("1");//编译通过
  7. arrayList1.add(1);//编译错误
  8. String str1=arrayList1.get(0);//返回类型就是String
  9.  
  10. ArrayList arrayList2=new ArrayList<String>();
  11. arrayList2.add("1");//编译通过
  12. arrayList2.add(1);//编译通过
  13. Object object=arrayList2.get(0);//返回类型就是Object
  14.  
  15. new ArrayList<String>().add("11");//编译通过
  16. new ArrayList<String>().add(22);//编译错误
  17. String string=new ArrayList<String>().get(0);//返回类型就是String
  18. }
  19. }

    通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

  从这里,我们可以再讨论下泛型中参数话类型为什么不考虑继承关系?

  在Java中, 像下面形式的引用传递是不允许的:

  1. rrayList<String> arrayList1=new ArrayList<Object>();//编译错误
  2. ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误

  我们先看第一种情况,将第一种情况拓展成下面的形式:

  1. ArrayList<Object> arrayList1=new ArrayList<Object>();
  2. arrayList1.add(new Object());
  3. arrayList1.add(new Object());
  4. ArrayList<String> arrayList2=arrayList1;//编译错误

  实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

  在看第二种情况,将第二种情况拓展成下面的形式:

  1. ArrayList<String> arrayList1=new ArrayList<String>();
  2. arrayList1.add(new String());
  3. arrayList1.add(new String());
  4. ArrayList<Object> arrayList2=arrayList1;//编译错误

  没错,这样的情况比第一种情况好的多,最起码,在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用arrayList2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

  所以,要格外注意,泛型中的引用传递的问题。

  2、自动类型转换

  因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下ArrayList和get方法:

  1. public E get(int index) {
  2. RangeCheck(index);
  3. return (E) elementData[index];
  4. }

  可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么,表达式:

  1. Date date=pair.value

也会自动地在结果字节码中插入强制类型转换。

  3、类型擦除与多态的冲突和解决方法

  现在有这样一个泛型类:

  1. class Pair<T> {
  2. private T value;
  3. public T getValue() {
  4. return value;
  5. }
  6. public void setValue(T value) {
  7. this.value = value;
  8. }
  9. }

  然后我们想要一个子类继承它

  1. class DateInter extends Pair<Date> {
  2. @Override
  3. public void setValue(Date value) {
  4. super.setValue(value);
  5. }
  6. @Override
  7. public Date getValue() {
  8. return super.getValue();
  9. }
  10. }

  在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

  1. public Date getValue() {
  2. return value;
  3. }
  4. public void setValue(Date value) {
  5. this.value = value;
  6. }

  所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

  分析:

  实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

  1. class Pair {
  2. private Object value;
  3. public Object getValue() {
  4. return value;
  5. }
  6. public void setValue(Object value) {
  7. this.value = value;
  8. }
  9. }

  再看子类的两个重写的方法的类型:

  1. @Override
  2. public void setValue(Date value) {
  3. super.setValue(value);
  4. }
  5. @Override
  6. public Date getValue() {
  7. return super.getValue();
  8. }

  先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。
  我们在一个main方法测试一下:

  1. public static void main(String[] args) throws ClassNotFoundException {
  2. DateInter dateInter=new DateInter();
  3. dateInter.setValue(new Date());
  4. dateInter.setValue(new Object());//编译错误
  5. }

  如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了。

  为什么会这样呢?

  原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

  1. class Pair {
  2. private Date value;
  3. public Date getValue() {
  4. return value;
  5. }
  6. public void setValue(Date value) {
  7. this.value = value;
  8. }
  9. }

  然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。

  可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

  于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法

  首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

  1. class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
  2. com.tao.test.DateInter();
  3. Code:
  4. 0: aload_0
  5. 1: invokespecial #8 // Method com/tao/test/Pair."<init>"
  6. :()V
  7. 4: return
  8.  
  9. public void setValue(java.util.Date); //我们重写的setValue方法
  10. Code:
  11. 0: aload_0
  12. 1: aload_1
  13. 2: invokespecial #16 // Method com/tao/test/Pair.setValue
  14. :(Ljava/lang/Object;)V
  15. 5: return
  16.  
  17. public java.util.Date getValue(); //我们重写的getValue方法
  18. Code:
  19. 0: aload_0
  20. 1: invokespecial #23 // Method com/tao/test/Pair.getValue
  21. :()Ljava/lang/Object;
  22. 4: checkcast #26 // class java/util/Date
  23. 7: areturn
  24.  
  25. public java.lang.Object getValue(); //编译时由编译器生成的巧方法
  26. Code:
  27. 0: aload_0
  28. 1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
  29. ;
  30. 4: areturn
  31.  
  32. public void setValue(java.lang.Object); //编译时由编译器生成的巧方法
  33. Code:
  34. 0: aload_0
  35. 1: aload_1
  36. 2: checkcast #26 // class java/util/Date
  37. 5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法
  38. )V
  39. 8: return
  40. }

  从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

  所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

  不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

    setValue方法是为了解决类型擦除与多态之间的冲突。

    而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

  那么父类的setValue方法如下:

  1. public ObjectgetValue() {
  2. return super.getValue();
  3. }

   而子类重写的方法是:

  1. public Date getValue() {
  2. return super.getValue();
  3. }

  其实这在普通的类继承中也是普遍存在的重写,这就是协变

  关于协变:。。。。。。

  并且,还有一点也许会有疑问,子类中的巧方法  Object   getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

  4、泛型类型变量不能是基本数据类型

  不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

  5、运行时类型查询

  1. ArrayList<String> arrayList=new ArrayList<String>();

  因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。

  那么,运行时进行类型查询的时候使用下面的方法是错误的

  1. if( arrayList instanceof ArrayList<String>)

  6、泛型在静态方法和静态类中的问题

  泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

  举例说明:

  1. public class Test2<T> {
  2. public static T one; //编译错误
  3. public static T show(T one){ //编译错误
  4. return null;
  5. }
  6. }

  因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:

  1. public class Test2<T> {
  2.  
  3. public static <T >T show(T one){//这是正确的
  4. return null;
  5. }
  6. }

  因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

  写一篇好博客不容易,转自:http://blog.csdn.net/wisgood/article/details/11762427

Java泛型-内部原理: 类型擦除以及类型擦除带来的问题的更多相关文章

  1. 关于Java泛型实现原理的思考与一般用法示例总结

    面向对象的一个重要目标是对代码重用的支持.支持这个目标的一个重要机制就是泛型机制.在1.5版本之前,java并没有直接支持泛型实现,泛型编程的实现时通过使用继承的一些基本概念来完成的. 这种方式的局限 ...

  2. java 泛型实现原理

    泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想.虽然思想一致,但是他们存在着本质性的不同. C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译 ...

  3. Java泛型中的类型参数和通配符类型

    类型参数 泛型有三种实现方式,分别是泛型接口.泛型类.泛型方法,下面通过泛型方法来介绍什么是类型参数. 泛型方法声明方式:访问修饰符 <T,K,S...> 返回类型 方法名(方法参数){方 ...

  4. java 泛型: 通配符? 和 指定类型 T

    1. T通常用于类后面和 方法修饰符(返回值前面)后面 ,所以在使用之前必须确定类型,即新建实例时要制定具体类型, 而?通配符通常用于变量 ,在使用时给定即可 ? extends A  :  通配符上 ...

  5. JAVA泛型实现原理

    1. Java范型时编译时技术,在运行时不包含范型信息,仅仅Class的实例中包含了类型参数的定义信息.泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的.你可以(基本上就是)把它 ...

  6. Java并发编程原理与实战七:线程带来的风险

    在并发中有两种方式,一是多进程,二是多线程,但是线程相比进程花销更小且能共享资源.但使用多线程同时会带来相应的风险,本文将展开讨论. 一.引言 多线程将会带来几个问题: 1.安全性问题 线程安全性可能 ...

  7. 浅析Java泛型

    什么是泛型? 泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型.这种参数类型 ...

  8. Java泛型总结---基本用法,类型限定,通配符,类型擦除

    一.基本概念和用法 在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化.例如在哈希表的存取中,JDK1.5之前使用HashMap的 ...

  9. Java——泛型

    前言 一般的类和方法,使用的都是具体的类型:基本类型或者自定义的类.如果我们要编写出适用于多种类型的通用代码,那么肯定就不能使用具体的类型.前面我们介绍过多态,多态算是一种泛化机制,但是也会拘泥于继承 ...

随机推荐

  1. 北京全景视频外包公司:长年承接VR全景视频外包

    北京动点飞扬软件,从事外包业务五年,长年承接全景VR视频,全景普通视频外包. 以下是全景VR视频案例(可操作,人不动景物不动,人移动,景物跟随) 欢迎联系我们QQ:372900288 TEL:1391 ...

  2. Lua学习----Lua的表达式

    前言 Lua的运算符和其他语言基本类似.但也有一点点区别 1.算术运算符 Lua的算术运算符加入了指数运算符^ print(2 ^ 10) -->打印 1024. 求2的10次方 2.关系运算符 ...

  3. fMRI数据分析处理原理及方法(转载)

    原文地址:http://www.cnblogs.com/minks/p/4889497.html 近年来,血氧水平依赖性磁共振脑功能成像(Blood oxygenation level-depende ...

  4. mysql数据导出excel格式+乱码解决

    1:导出的SQL命令,只需要加上“FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' ” ...

  5. GBDT算法原理深入解析

    GBDT算法原理深入解析 标签: 机器学习 集成学习 GBM GBDT XGBoost 梯度提升(Gradient boosting)是一种用于回归.分类和排序任务的机器学习技术,属于Boosting ...

  6. c++智能指针实现方式1

    #include<iostream> using namespace std; // 定义仅由HasPtr类使用的U_Ptr类,用于封装使用计数和相关指针 // 这个类的所有成员都是pri ...

  7. VirtrualBox使用已存在的镜像创建虚拟机

    再将一个已经存在的虚拟机镜像拷贝为另一个新的虚拟机镜像后,要将该新的镜像添加到新的虚拟机中时会出现错误提示,从而导致不能创建虚拟机.例如有'D:\App\VirtualBox VMs\CentOS_6 ...

  8. C#动手实践:Kinect V2 开发(2):数据源工作原理及红外源Demo

    Kinect体系架构

  9. 简单了解undo

    ORACLE 数据库 DML语句执行之前,会将数据块修改前的信息 镜像保存到 undo 段 insert语句     rollback    将会执行一条deleter操作  (相对数据量最小,只需利 ...

  10. 转I2s

    转自http://blog.csdn.net/ce123/article/details/6919954 I2S音频总线学习(二)I2S总线协议 一.I2S总线概述 音响数据的采集.处理和传输是多媒体 ...