java泛型(二)--泛型的擦除

2019-04-14 09:08发布

相信通过上一篇泛型相关的文章,大家对泛型有了一个大致的了解,现在我们来简单的看一个小例子: public class GenericEraseTest { public static void main(String[] args){ ArrayList stringList = new ArrayList(); ArrayList intList = new ArrayList(); System.out.println(stringList.getClass() == intList.getClass()); } }
上面的代码打印出的应该是什么?根据我们上一次泛型基础所了解到的,int类型的元素是无法添加到stringList中的,按正常的思维,打印出的值应该是false,因为很明显两个类的行为不同(接受的参数类型不同)。但是结局又是让人崩溃的,打印出了true。我们将上面的java代码编译成class文件,然后再反编译出来结果如下所示: public class GenericEraseTest { public static void main(String[] args){ ArrayList stringList = new ArrayList(); ArrayList intList = new ArrayList(); System.out.println(stringList.getClass() == intList.getClass()); } }现在看起来应该熟悉多了,打印出的true也应该是在意料之中了。但是为什么这样?这就引出了今天的第一个概念: 泛型的擦除 在java语言中,泛型只存在于源代码中,而在字节码中泛型类都被替换为原生类,在运行期所操作的类型也都是原生类,这种特性我们称之为擦除。相信有了上面的实例,不难理解这句话的意思。我们来想一下java的泛型为什么通过擦除来实现? 在C++或者C#中,泛型无论是在源码,还是在编译的中间代码,亦或者是在运行期中,泛型都是真实存在的,我们都可以正常的使用它,List和List就是两个不同的类,但是在java中并不是这样的。关于在java中为什么利用擦除来实现泛型我了解的有大概两种说法: 1.对兼容性方面的考虑。在Thinking in java 一书中作者说了如下一段话: “为了减少潜在的关于擦除的混淆,你必须清楚的认识到这不是一个语言特性,它是java的泛型实现中的一种折中。如果泛型在Java1.0中就已经是其一部分了,那么这个特性将不会用擦除来实现——它将使用具体化” 在java1.5以后的版本中,即使引入了泛型的概念,我们也必须使其能兼容之前在没有泛型时所编写的类库。而之前所写的代码也要能在泛型加入类库中去时继续保持可用。 2.由于在C++或C#中泛型是真实存在的,List和List将生成两个不同的类,这样很容易导致类膨胀的问题,使得代码编译的速度降低。 上面两种说法都有自己的道理,也无法去深究其对错,而我们要做的是理解它本质的含义,以便在使用时可以得心应手。
泛型擦除所带来的影响 在泛型代码的内部,我们无法获得任何有关泛型参数类型的信息,虽然能得到类型的参数标识,但是并不能用来创建实例。这句话看起来比较抽象,什么是参数类型信息,什么是参数标识还是一头雾水,没关系,我们看下面几个例子: public T get() { T t = new T(); return a; }
如果我们在代码中写了类似上面的语句,那么编译器报错,并且提示如下的语句“Cannot instantiate the type T”。 if(T instanceof String){ //xxxxx }
如果我们在代码中这样写,编译器同样也会报错“T cannot be resolved”。 现在大家应该可以明白上面所说的不能获得任何参数类型的信息,也不能用来创建实例hi什么意思了吧。但是将其作为类型来转型还是可以的,比如说这样: public T get() { Object obj = new Object(); return (T)obj; }编译器只是报了一个转型的警告但是并没有阻止,这段代码也解释了上面提到的可以获得类型的参数标识。 为了分析内部的原因,我们引入一个简单的Holder类代码: public class Holder { private T a; public Holder(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { Holder holder = new Holder("123"); String string = holder.get(); System.out.println(string); } }以下是反编译出来的执行过程:
Compiled from "Holder.java" public class com.fsc.generic.Holder { public com.fsc.generic.Holder(T); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."": ()V 4: aload_0 5: aload_1 6: putfield #2 // Field a:Ljava/lang/Object; 9: return public void set(T); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field a:Ljava/lang/Object; 5: return public T get(); Code: 0: aload_0 1: getfield #2 // Field a:Ljava/lang/Object; 4: areturn public static void main(java.lang.String[]); Code: 0: new #3 // class com/fsc/generic/Holder 3: dup 4: ldc #4 // String 123 6: invokespecial #5 // Method "":(Ljava/lang/Objec t;)V 9: astore_1 10: aload_1 11: invokevirtual #6 // Method get:()Ljava/lang/Object; 14: checkcast #7 // class java/lang/String 17: astore_2 18: getstatic #8 // Field java/lang/System.out:Ljava/ io/PrintStream; 21: aload_2 22: invokevirtual #9 // Method java/io/PrintStream.printl n:(Ljava/lang/String;)V 25: return }
我们可以从中看到两点比较重要的内容:1.在调用set方法传递参数的时候是以Object对象来接收的。2.在调用get方法进行返回时返回的是Object,仍然需要转型,只不过转型不是手动的而已,是编译器自动帮我们插入的。通过上面的代码我们也应该明白了为什么不能使用new关键字来创建T类型的对象了,因为T根本就不存在,在类中针对T的方法操作其实都是针对Object来的。 在对泛型的使用中,我们失去了它的参数类型信息,不能用来创建对象以及类型的比较,接下来提供一种思路来处理这种问题: public class Holder { private Class cls; public Holder(Class cls) { this.cls = cls; } public T getInstance(){ T newInstance = null; try { newInstance = cls.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return newInstance; } public boolean isInstance(Object obj){ return cls.isInstance(obj); } public static void main(String[] args) { Holder holder = new Holder(String.class); boolean isInstance = holder.isInstance("123"); System.out.println(isInstance); String instance = holder.getInstance(); System.out.println(instance); } }我们重新定义了一个Holder类,只不过它存储的东西编程了具体类型的Class对象,这样我们就能通过这个Class对象来在一定程度上来弥补泛型类所带来的缺陷。当然这也仅仅只是一段示例,若要真正使用,还需要处理很多问题。 泛型数组的创建 通过前面的学习我们知道,在泛型内无法得到泛型参数类型的信息,那么我们如何创建出泛型参数类型的数组呢? 由于泛型的类型信息在运行期被擦除掉了,在有泛型类型参与的地方全部变为Object(当然也有可能是其他的类,在下一篇文章中会介绍),那么我们是不是可以考虑创建出一个Object类型的数组,然后将其转型储存起来,就像下面这样: public class GenericArray { private T[] array; @SuppressWarnings("unchecked") public GenericArray(int size){ array = (T[]) new Object[size]; } public T[] getArray(){ return array; } public static void main(String[] args) { GenericArray genericArray = new GenericArray(2); String[] array2 = genericArray.getArray(); } }
在创建数组的时候因为涉及到了转型信息,所以使用注解抑制了警告。运行上面的程序将会发现报错了,错误如下: Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;错误已经说的很清楚了,不能将Object类型的数组转换成String类型的数组 我们再来看另外一种写法: public class GenericArray { private Object[] array; public GenericArray(int size){ array = new Object[size]; } @SuppressWarnings("unchecked") public T[] getArray(){ return (T[]) array; } public static void main(String[] args) { GenericArray genericArray = new GenericArray(2); String[] array2 = genericArray.getArray(); } }将转型的位置换了地方,在泛型数组中用Object数组来存放数据,但是很不幸,仍然报了和刚才一样的错误。虽然这种写法仍然报错,但是如果仔细查看java的源码就会发现ArrayList使用的就是这种方式,尽管它没有向我们提供接口来返回内部的数组。 下面再来看一种写法: public class GenericArray { private T[] array; private Class cls; @SuppressWarnings("unchecked") public GenericArray(int size, Class cls){ this.cls = cls; array = (T[]) Array.newInstance(cls, size); } public T[] getArray(){ return array; } public static void main(String[] args) { GenericArray genericArray = new GenericArray(2,String.class); String[] array2 = genericArray.getArray(); } }
运行结果一切正常,使用这种方式来创建泛型数组是可取的。再者,在我们想使用泛型数组的时候可以直接使用容器,容器也是支持泛型的,所以和使用数组的感觉没什么太大的不同。

在下一篇文章中将会详细的介绍泛型的边界,通配符,以及一些总结。