public class Box<T> {
private T element;
public T getElement() {
return element;
}
public void setElement(T element) {
this.element = element;
}
}
public class Test {
public static void main(String[] args) {
List<Box> l = new ArrayList<>(); //Just List of Box with no specific type
Box<String> box1 = new Box<>();
box1.setElement("aa");
Box<Integer> box2 = new Box<>();
box2.setElement(10);
l.add(box1);
l.add(box2);
//Case 1
Box<Integer> b1 = l.get(0);
System.out.println(b1.getElement()); //why no error
//Case 2
Box<String> b2 = l.get(1);
System.out.println(b2.getElement()); //throws ClassCastException
}
}
列表l
包含Box
类型的元素。在第一种情况下,我将第一个元素作为Box<Integer>
,在第二种情况下,列表中的第二个元素作为Box<String>
获得。在第一种情况下不抛出ClassCastException。
当我尝试调试时,element's
和b1
中的b2
类型分别为String
和Integer
。
是否与类型擦除有关?
答案 0 :(得分:4)
确切地说,问题是PrintStream#println
。
让我们使用javap -c Test.class
检查已编译的代码:
72: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
75: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
正如您所看到的,编译器删除了类型并省略了Integer
的强制转换,因为这里没有必要。编译器已将使用的重载methoded链接到PrintStream#(Object)
。
由于JLS rule §5.3:
方法调用转换应用于方法或构造函数调用(§8.8.7.1,§15.9,§15.12)中的每个参数值:参数的类型表达式必须转换为相应参数的类型。
方法调用上下文允许使用以下之一:
第三条规则是从子类型转换为超类型:
如果S是T的子类型(§4.10),则从任何引用类型S到任何引用类型T都存在扩展引用转换。
在检查之前是否可以取消装箱(第五次检查:“取消装箱转换”)。因此,编译器检查Integer
是Object
的子类型,因此必须调用#println(Object)
(如果检查被调用的重载版本,IDE将告诉您相同的内容)。
另一方面是第二个版本:
95: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
98: checkcast #14 // class java/lang/String
101: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
有checkcast
来检查检索到的Box#getElement
类型是否真的是String
。这是必要的,因为您告诉编译器它将是String
(由于泛型类型Box<String> b2 = l.get(1);
)并且它链接了方法PrintStream#(String)
。此检查因提及ClassCastException
而失败,因为无法将Integer
强制转换为String
。
答案 1 :(得分:3)
好的,这里的问题是b2 错误地标记为Box<String>
当它实际上Box<Integer>
(box2的类型)时 - 所以{{ 1}}被输入为String,即使它实际上包含一个Integer。编译器尝试调用重载的println方法,该方法接受String而不是接受Object的方法,因此您获得ClassCastException。 println的Object版本将其参数显式转换为String(通过调用toString()),但该方法的String版本不会这样做。
基本问题是使用原始类型而不是完全指定列表b2.getElement()
的类型参数 - 它应该是l
。然后,您已将List<Box<?>>
和b1
作为Box,并且已选择System.out.println的正确重载。
答案 2 :(得分:1)
在编译时,编译器知道类型,并使用正确的参数类型将System.out.println(..)
的调用链接到方法。在第一种情况下,编译器解析对println(Object)
的调用。由于b1.getElement()
返回Object
,String
是Object
,因此方法调用是正确的,并且不会引发任何异常。在第二种情况下,编译器解析对println(String)
的调用,因为Box<String>
,但b2.getElement()
返回Integer
。这不能转换为String,并且会引发ClassCastException
。
答案 3 :(得分:0)
没有为Integer参数定义println方法,所以你的代码将调用println(Object object),它将调用object.toString()来获取打印出来的字符串。没有类型检查,因为一切都是对象。
在第二种情况下,你的代码想要调用println(String someString),因此它将检查someString是否真的是一个String,因为它不会引发异常。