编译器真的擦出了集合的“类型”信息了吗
来源:互联网 发布: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类等,那么你会发现一些规律的。
- 编译器真的擦出了集合的“类型”信息了吗
- 你擦了吗?确定擦了?真的确定擦了?
- 你擦了吗?确定擦了?真的确定擦了?
- HG - 当Hexo遇到Github,擦出了什么样的火花
- 百度真的出了瑞丽算法吗
- 我擦咧!SourceForge真出问题了!
- 你真的理解了Collection和Map集合吗?
- 黑马程序员-泛型的擦出
- 泛型-擦出的神秘之处
- 学会了 C 语言真的可以开发出很多东西吗?
- 学会了 C 语言真的可以开发出很多东西吗?
- 真的可以了
- 真的倒了
- 真的落伍了。
- 真的长大了!
- 真的渺茫了!!!!!!
- 真的OUT了
- 真的痴迷了
- hdoj 1501 (import,本来以为这是个搜索题,后来发现是一个dp,而且第一次还没有设置状态)
- C# 实现优酷视频弹出小窗口播放
- linux的终端音乐播放器moc
- Hibernate 的 Session接口
- MFC Static控件背景透明时文本覆盖重影
- 编译器真的擦出了集合的“类型”信息了吗
- Internal Sales Order (ISO) Process Flow
- 说到做到,严己宽人,只认功劳,不认苦劳-
- MFC ClientToScreen 和 ScreenToClient
- Jquery为单选框checkbox绑定单击事件
- json
- 面试题
- 手机:运行内存,机身内存,内存卡的区分
- S3C2440 madplayer mp3移植笔记