黑马程序员技术交流社区

标题: 以前看到的一个问题,关于泛型被反射擦除的异常 [打印本页]

作者: a6511631    时间: 2014-8-20 20:47
标题: 以前看到的一个问题,关于泛型被反射擦除的异常
本帖最后由 a6511631 于 2014-8-20 22:41 编辑

为什么图中红线的部分可以 ,下面的就类转换异常呢。求解释,求详细。

补充:
           问题具体描述:泛型写成<Integer>不报异常,写<String>却会报类型转换异常?
  1. //                ArrayList<Integer> list = new ArrayList<Integer>();
  2. //                list.getClass().getMethod("add", Object.class).invoke(list, "1234");
  3. //                System.out.println(list.get(0));
  4.                 ArrayList<String> list2 = new ArrayList<String>();
  5.                 list2.getClass().getMethod("add", Object.class).invoke(list2, 123);
  6.                 System.out.println(list2.get(0));
复制代码




010544j0gi6n6ccsic7wu9.png.thumb.jpg (10.16 KB, 下载次数: 193)

010544j0gi6n6ccsic7wu9.png.thumb.jpg

作者: justin1258    时间: 2014-8-20 20:47
本帖最后由 justin1258 于 2014-8-21 17:18 编辑

这个问题其实不仅涉及到类型擦除,还涉及到方法重载,下面我试着结合这两点以及实际的字节码为证来解释楼主的问题:
为了方便说明,从第二段代码开始说,这段代码会报错:
  1. ArrayList<String> list2 = new ArrayList<String>();
  2.         list2.getClass().getMethod("add", Object.class).invoke(list2, 123);
  3.         System.out.println(list2.get(0));
复制代码


下面是对上面代码的解释:
1.用带类型参数的方式定义list变量,类型变量的作用是显式地告诉编译器list是用来装String的,那么就可以获得编译期间的类型检查。编译器在必要的时候会 插入类型转换代码,比如String s = list.get(0),编译器就会把这句代码编译成:String i = (String)list.get(0)。可以看到实质上是编译器帮我们做了转换工作,这是泛型最明显的好处。但是当你声明成:ArrayList list = new ArrayList<String>();的时候就不会有这样的好处,因为编译器看到list是不带泛型参数的原始类型ArrayList,他不知道相关的类型信息,这就要我们手动转换了。
2.第二句代码其实就是将 123 添加到list中,这是允许的,有两个原因:
  1.通过反射的方式调用方法并不会受到编译期的类型检查,只会在运行时进行类型检查。
  2.list类型擦出后其实内部装的还是Object类型元素.
但是这句话无论在什么时候都不会报错,因为在编译期和运行时他都合法。
3.第三句是重点,由于编译器知道list.get(int)返回的是String类型(为什么?上面说过因为受泛型参数的影响)。下面就是寻找调用System.out.println重载方法的过程了,编译知道了参数类型为String这点之后就要去找是否有这个“精确匹配的方法”,确实有,(如何确定重载方法这里不讲),那么就会调用System.out.println(String)这个方法。那么这里编译器就要插入类型转换的代码了,就像这样System.out.println((String)list.get(0));这里就是运行时报ClassCastException异常的原因,因为list.get(0)取出来的实际类型是Integer(因为装箱)。
下面是:相关字节码,只要看用红框标注的就行,第51行checkcast就是类型转换指令:


下面的截图是第54行字节码调用的println方法的具体描述,可以确定他调用的是println(string)重载:


靠,写了那么多。


接下来是第一段代码:
  1. ArrayList<Integer> list = new ArrayList<Integer>();
  2.         list.getClass().getMethod("add", Object.class).invoke(list, "1234");
  3.         System.out.println(list.get(0));
复制代码
额,敲了那么多不是白敲的,有了上面的基础,这里很多地方就不用解释了。直接解释第3句:
这里也是要确定调用按个pringtln重载方法,这时因为编译器知道list.get(0)返回的是Integer,那么编译器首先去找有没有精确匹配的方法Println(Integer),很遗憾,木有。。。那么编译器就要找找有没有其他合适的方法了,找啊找,于是找到了一个比较合适的方法:println(Object)。此时编译器就要调用这个方法了,但是这里和上面的不同,由于Integer类型可以向上转型为Object,所以编译器这里并不会插入类型转换代码,直接调用。。所以啊所以,最终就不会报ClassCastException了。下面是相关的字节码,我们看到编译器并没有生成转型代码,而是直接调用println方法:


具体的重载方法描述:



综上所述,原因在于编译器需要根据list.get(int)放回的类型来确定重载方法,并且在必要的时候进行转型。
如果楼主试着把上面第一段代码的最后一句代码改成:
Integer integer = list.get(0);
System.out.println(integer);

那么同样要报错。

作者: masai158    时间: 2014-8-20 21:18


这难道跟脸蛋有关系?

作者: a6511631    时间: 2014-8-20 21:26
masai158 发表于 2014-8-20 21:18
这难道跟脸蛋有关系?

注意看别眨眼。我的打印不是直接打印list哟。
作者: masai158    时间: 2014-8-20 21:34
本帖最后由 masai158 于 2014-8-20 21:37 编辑
a6511631 发表于 2014-8-20 21:26
注意看别眨眼。我的打印不是直接打印list哟。


mark ......好了。当我没说过。待人回答。。
我上面抛异常了

作者: a6511631    时间: 2014-8-20 21:45
本帖最后由 a6511631 于 2014-8-20 21:48 编辑
masai158 发表于 2014-8-20 21:34
mark ......好了。当我没说过。待人回答。。
我上面抛异常了定义Method的时候写"Object.class"参数不报异常,写"String.class"参数却报异常?
哎,不能省事啊,我改一下帖子,把问题描述的更具体好了。
作者: masai158    时间: 2014-8-20 21:51
个人简单的理解吧。。。看 invoke 函数。他都抛异常。
学异常是什么???有异常要么 抛 要么 catch。。
当人家抛了3个异常。你觉得你该干什么???



作者: masai158    时间: 2014-8-20 22:06
既然是去泛型了。那么请问。。 集合中。有     add(添加字符串的方法)? NoSuchMethodException : 无法找到某一特定方法时,抛出该异常。



作者: a6511631    时间: 2014-8-20 22:30
masai158 发表于 2014-8-20 22:06
既然是去泛型了。那么请问。。 集合中。有     add(添加字符串的方法)? NoSuchMethodException : 无法 ...

被先前的人搞混了,靠,问题描述写错了,已改了描述,这才是问题的愿意,求解答
作者: Bule丶    时间: 2014-8-20 22:41
和添加的(al,"abc");abc是String类型有关吗
作者: Bule丶    时间: 2014-8-20 22:45
ArrayList<Integer> list = new ArrayList<Integer>();

list.getClass().getMethod("add", Object.class).invoke(list, "1234");
你的1234被"包围了,代表就是字符串,如果把"去掉,就应该可以,你试试
作者: HKing    时间: 2014-8-20 22:51
本帖最后由 HKing 于 2014-8-20 22:56 编辑

String类型数据存放在常量池中,一旦定义就不能被改变!所以
  1. ArrayList<String> al = new ArrayList<String>();
  2. al.getClass().getMethod("add", Object.class).invoke(al, 13)
复制代码
会报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
你将代码改成下面这个样子就OK了!
  1. ArrayList<StringBuilder> al = new ArrayList<StringBuilder>();
  2. al.getClass().getMethod("add", Object.class).invoke(al, 13);
复制代码








作者: a6511631    时间: 2014-8-20 23:00
HKing 发表于 2014-8-20 22:51
String类型数据存放在常量池中,一旦定义就不能被改变!所以会报错:
Exception in thread "main" java.lan ...

额,这个问题的代码定义了String类型数据吗?与问题的联系在哪里呢?
第二种情况中,我直接打印System.out.println(list2);它没报错
换成System.out.println(list2。get(0));就会报异常了。
我知道问题是出现String类型上,但是你这个解释不够详细。



作者: HKing    时间: 2014-8-20 23:28
本帖最后由 HKing 于 2014-8-21 01:07 编辑
a6511631 发表于 2014-8-20 23:00
额,这个问题的代码定义了String类型数据吗?与问题的联系在哪里呢?
第二种情况中,我直接打印System.ou ...

我试了下,发现单独调用al.get(0);并不会报错,而一旦使用String类里面的方法时,就会报错。这就像是一个String类对象里面装的是一个Integer对象,这导致它既不能调用String类的方法,也不能调用Integer方法,就是一个四不像。可以通过类型转换将其转换
  1. Object obj = (Object)al.get(0);
  2. Integer i = (Integer)obj;
  3. System.out.println(i);
复制代码
至于里面的详原因就不清楚了。:lol


作者: a6511631    时间: 2014-8-21 08:41
本帖最后由 a6511631 于 2014-8-21 08:42 编辑
HKing 发表于 2014-8-20 23:28
我试了下,发现单独调用al.get(0);并不会报错,而一旦使用String类里面的方法时,就会报错。这就像是一个S ...

我也是没懂这里的原因是什么才发帖求解释的
作者: 逍遥客    时间: 2014-8-21 09:07
类型不匹配
作者: java_dream    时间: 2014-8-21 10:49
本帖最后由 java_dream 于 2014-8-21 10:54 编辑

     list2.getClass().getMethod("add", Object.class).invoke(list2, 123);通过getMethod方法获取的是方法签名为add,参数类型是Object.class的方法,在调用invoke是传入的是123,123会自动装箱为Integer类型,而Integer类型是Object的子类,所有此处程序没有问题。(如果调用getMethod方法是传入的参数类型是String.class,因为Integer不是String的子类,所以invoke时会发生异常。这可以论证上面的说法)
    ArrayList<String> list2 = new ArrayList<String>();声明了一个变量list2,传入的类型参数是String,所以list2对象的方法的返回值类型是String.class,所以执行list2.get(0)会发生 java.lang.ClassCastException异常。
作者: a6511631    时间: 2014-8-21 10:55
java_dream 发表于 2014-8-21 10:49
list2.getClass().getMethod("add", Object.class).invoke(list2, 123);通过getMethod方法获取的是方 ...

说实话,我被你说晕了,你按照你的逻辑说说为什么第一种情况是可以的呢
作者: java_dream    时间: 2014-8-21 11:24
不好意思,是我欠考虑了
作者: supertoy    时间: 2014-8-21 11:47
①第一个调用了System.out.println(Object o):方法:【源码调用了String.valueOf(x);】所以不会出错。
②第二个调用了System.out.println(String s)方法:运行时,list.get()取出来都是Object,然后强转String,出错。
----------------
---------------
生成的class文件:
  第一个:
   45  invokevirtual java.util.ArrayList.get(int) : java.lang.Object [48]
   48  invokevirtual java.io.PrintStream.println(java.lang.Object) : void [52]
------
   99  invokevirtual java.util.ArrayList.get(int) : java.lang.Object [48]
102  checkcast java.lang.String [64]               
☆   105  invokevirtual java.io.PrintStream.println(java.lang.String) : void [66]


作者: fantacyleo    时间: 2014-8-21 12:15
现在可以确定的是,当泛型设定为String时,get方法取出的时候会尝试将类型转换为String,并调用System.out.println(String)这个重载版本的方法,这一转就异常了。而泛型设定为非String时,不会尝试转换,并调用System.out.println(Object)这个重载版本的方法
作者: Louis.hui    时间: 2014-8-21 17:13
a1你初始化的是个“abc”字符串,而你拟对象时是Integer,所以不匹配,就会出现语法异常
作者: Louis.hui    时间: 2014-8-21 17:16
ArrayList<Integer>a1=new ArrayList<Integer>();
作者: Louis.hui    时间: 2014-8-21 17:32
这是集合框架问题,list一个序列接口,而知识单一a1对象,ArrayList类是实现了可变大小的数组,所以就会出现语法错误
作者: Louis.hui    时间: 2014-8-21 17:37
这是集合框架问题,list一个序列接口,而知识单一a1对象,ArrayList类是实现了可变大小的数组,所以就会出现语法错误,如果a1是个数组类型,就会没问题




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2