编译器真的擦出了集合的“类型”信息了吗

来源:互联网 发布:bing桌面壁纸软件 编辑:程序博客网 时间:2024/05/01 14:16

大家都知道,在集合上使用泛型,可以大大的减少运行时期出现的错误,并且效率还不会降低。有这两个方面的原因:

第一、在定义集合的时候使用泛型,会在编译时期就限定了传入集合中元素的类型,所以保证了传入元素的类型。

第二、运行之前,编译器会将集合上的限定类型擦除,就像没有加入限定类型一样,从而效率不会降低。

可是,大家还知道,反射是会越过编译器,从而加入限定类型以外的其他类型,这是反射强大的一方面。看个例子就知道了:

@Testpublic void test5() throws Exception {ArrayList<Integer> list = new ArrayList<Integer>();Method method = list.getClass().getMethod("add", Object.class);method.invoke(list, "abc");System.out.println(list.get(0));}

但是,你真的认为编译器会去掉“类型”信息吗?这种擦除真的实现了吗?我表示怀疑,请看下面的例子:

下面有四个测试:

测试1

@Testpublic void test1() throws Exception {//限定为Integer类型List<Integer> list = new ArrayList<Integer>();System.out.println(list.getClass().getName());//java.util.ArrayListlist.add(12);//反射获得add方法,并加入非限定类型元素Method method = list.getClass().getMethod("add", Object.class);method.invoke(list, "aaa");System.out.println(list.getClass().getName());//java.util.ArrayListSystem.out.println(list.get(1));//结果为:aaa/*  System.out.println(list.get(1).getClass().getName());     报错:  java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String *///分段转换Object ob = list.get(1);String str = (String)ob;System.out.println(str);//结果为:aaa//直接转换--->报错:Cannot cast from Integer to String//String str2 = (String)list.get(1);}
测试2

@Testpublic void test2() throws Exception {//限定为Integer类型ArrayList<Integer> list = new ArrayList<Integer>();//反射获取add方法,可以加入任意类型值Method method = list.getClass().getMethod("add", Object.class);method.invoke(list, "bbb");System.out.println(list.getClass().getName());//java.util.ArrayListSystem.out.println(list.get(0));//结果为:bbb/*  System.out.println(list.get(0).getClass().getName());           报错:    java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String *///反射获取get方法Method method1 = list.getClass().getMethod("get", int.class);String str = (String)method1.invoke(list, 0);System.out.println(str);//结果为:bbb}

测试3

@Testpublic void test3() throws Exception {//限定为String类型ArrayList<String> list = new ArrayList<String>();//反射获得add方法,并加入非限定类型数据Method method = list.getClass().getMethod("add", Object.class);method.invoke(list, 123);/*System.out.println(list.get(0));报错:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String *///分段转换Object obj = list.get(0);Integer num = (Integer)obj;System.out.println(num);//直接转换--->报错:Cannot cast from String to Integer//Integer i2 = (Integer)list.get(1);}

测试4

@Testpublic void test4() throws Exception {//限定为String类型ArrayList<String> list = new ArrayList<String>();//反射获得add方法,并加入非String类型的值Method method = list.getClass().getMethod("add", Object.class);method.invoke(list, 123);//反射获取get方法Method method2 = list.getClass().getMethod("get", int.class);Integer num = (Integer)method2.invoke(list, 0);System.out.println(num);//123System.out.println(method2.invoke(list, 0).getClass().getName());//java.lang.Integer}

一、问题出来了

比较测试1和测试2(注意:限定类型都为Integer)

1、在测试1中,直接打印list.get(1)是可以通过编译的,但是当打印list.get(1).getClass().getName()时就报错了,

2、如果直接强转并赋给String就会出问题,但是“分段转换”后,就能通过;另外通过反射获取get方法,就能直接获得“非法”加入的“bbb”这个字符串。

比较测试1和测试2(注意:测试3限定类型为String)

1、在测试1中,直接打印list.get(0)是可以通过编译的;而测试3中直接打印就出问题了

2、测试1和3的“分段转换”都是可以通过的,但是直接强转赋值,就会无法通过


是不是比较了这几个之后,你也会怀疑了呢?难道真的没有完全擦出吗?

我本以为真的没有完全擦除,但是我有比较了一下,只要是通过反射得到的结果都是可以直接获得的。而list直接调用的只有限定了String类型的集合是不能通过编译的。

其实确实擦除了“类型”信息。但是对于String很特殊。

二、我的想法

我来一个一个分析:

1、测试1中可直接打印 list.get(1),就可以通过,而不能直接得到 list.get(1).getClass().getName()的结果,为什么呢?

我个人认为,集合限定的类型是Integer,get()本身返回的也是Integer;如果单独打印list.get(1),是可以自动转为Object打印的,数据封装到了Object中,就不存在由Integer转换为String的问题了。

但是对于获取其字节码就不同了,因为get()返回的是Integer类型,但是从集合中获取位置1上的是String类型的值,我们可以将list.get(1)看成一个对象,那么要调用它的getClass()方法,这时候,编译器就开始检查了,他只知道get()返回的是Integer,不知道里面装了String的值,但是在运行时候就出了问题,JVM虚拟机,发现了 get()本身返回的是Integer,而获取到的却是你非法传入的String类型,虽然欺骗编译器,但是运行时被发现了,因此就不能调用它的方法了,所以就报错了。 

那么,难道就没有办法获取到list.get(1)的类型了吗?答案是可以获得

那就再欺骗一次JVM虚拟机呗,让他误认为是Object类型就可以了,其实还是将数据封装到Object中,像下面的一样:

System.out.println("get(1):" + ((Object)list.get(1)).getClass().getName());
这样就可以获得想要的结果了:get(1):java.lang.String

2、可是分段处理就不一样了,由于确实将类型具体落实到了各自的类型上,那么该是什么类型就是什么类型,因此就不会报错的。

3、为什么反射就能够直接获得想要的结果呢?

其实也是一个道理,我们观察就能发现,invoke()这个方法返回的类型是Object类型的,我们通常情况下都会强转的。我个人认为invoke()其实也自动将任何类型都作为Object返回的,也就是说它屏蔽了方法本身的类型,直接把存入的数据作为Object返回了,所以就能直接强转,赋值给指定传入的数据的类型了。

4、在测试3中,为什么直接打印list.get(0)就不行呢?

我们发现,测试3中集合限定的类型是String,难道String就那么特殊吗?

这个和测试1中的不同,似乎没有封装到Object中,在运行时被发现传入了非法类型值,所以就报了错。这个是String的特殊之处。究其原因,我个人认为是String比较特殊,没有将数据封装到Object中,从而导致了在运行时,get()本身返回的String类型和传入的Integer类型不符,就报了错:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这个的解决办法还是欺骗JVM虚拟机:

System.out.println((Object)list.get(0));

这样就能通过了,其实就是屏蔽get()本身的类型,将数据作为Object打印出来,就不会报错了。


三、总结:

1、查看ArrayList的API文档,你就可以发现,其中的ArrayList是这样定义的:ArrayList<E>,而其中的add方法定义为:boolean add(E e)、E get(int index),你是不是发现了什么。

我个人认为,虽然编译器擦除了“类型”信息,反射可以越过编译器,从而传入“非法值”;不过,反射是脆弱的;即使编译器擦出了集合的类型信息,但是在某种程度上来说,其中的方法可能还会保留原始限定的类型。比如add方法,参数类型虽然定义了E,要和上面的一样,但是在使用反射的时候,传入的类型参数必须是Object.class。如果不是的话,虽然编译时期并没有问题,但是运行时期是会报错的。

另外,其实从测试3中报的错:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String之中,我们不难看出,get()本身是保留着各种类型信息,如返回值类型,参数类型等等。之前get本身返回的应该是String类型,被保留在了get方法中;因此非法传入Integer类型,就会报错了。
2、对于返回类型和集合上限定的类型有关的,即传入了具体的“参数化类型”,如Integer(也就是定义了E的泛型集合):

第一、通过反射加入了非限定类型的数据,直接调用这个方法的,并不用强转;如list.get(0)

第二、而如果调用了这个方法又调用了其他方法,如list.get(0).getClass(),那么需要将list.get(0)作为一个整体强转为Object,即将数据封装到Object中,才能调用其他方法;否则报错。

3、对于返回类型与集合无关的,如size这个方法,无需强转,直接获取。因为size()的返回类型是int,并不是定义的E。
4、很重要的一点:除了String类型以外的其他类型(包括自定义类型),无需强转,可以直接使用相应集合中的方法。如上面的get()方法。我个人理解,与集合有关的方法,如get方法只有在你添加进元素时,才能调用,所以,即使get定义为:E get(int index),也无需强转。这里,String类型除外。
5、但是对于String类型呢?作为String类型的值,如果添加入限定为非String集合中,那么就需要强转,究其原因,我只能说String是比较特殊的一个类,它有如下特性:被定义为final,而且其值是存在了常量池中,并且可以将任意类型值通过字符串打印。基于如此特殊的类,那么,它必然也有某些特性。所以,我个人认为:在运行时是会检查的,并不是将数据封装到了Object中,而是会检查像get()这样的方法本身的类型和传入集合中的数据的类型的。


四、收获

通过这个测试,我自己给反射的作用起了两个名字:“自动包装”和“屏蔽类型”。也就是说反射出来的成员,都是作为Object返回的,这就屏蔽了成员本身的类型,从而切断了和其中数据的关联,也不是说分隔了成员类型和其中的数据类型。如上面说的get()本身返回的是E这个类型,而获取到的是非法传入的其他类型。

从中,我也看到了反射的脆弱,并不是随便通过反射传入其他值,就能成功通过运行的,在某种程度上,反射并不能做到完全的屏蔽,对于String如此特殊的类型,反射就显得有些脆弱了。所以在使用反射的时候,需要特别注意,如果不是用在框架中,尽量不用反射。另一方面,我也认识到了String类型的特殊之处,String中的很多方法都重写了Object中的方法,是不是可以从这里找到String的特殊之处的答案呢?有待研究。


后续:

大家也可以自己尝试着将集合的限定类型设定为其他类型,如Person类等,那么你会发现一些规律的。


原创粉丝点击