Java - 将通用对象获取为String通用类型会引发异常

时间:2016-03-26 08:46:09

标签: java generics type-erasure generic-collections

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'sb1中的b2类型分别为StringInteger

是否与类型擦除有关?

Ideone link

4 个答案:

答案 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)中的每个参数值:参数的类型表达式必须转换为相应参数的类型。

     

方法调用上下文允许使用以下之一:

     
      
  • 身份转换(§5.1.1
  •   
  • 扩大原始转化(§5.1.2
  •   
  • 扩大参考转化(§5.1.5
  •   
  • 拳击转化(§5.1.7),然后可选择加宽参考转换
  •   
  • 取消装箱转化(§5.1.8),可选地后跟扩展的原始转化。
  •   

第三条规则是从子类型转换为超类型:

  

如果S是T的子类型(§4.10),则从任何引用类型S到任何引用类型T都存在扩展引用转换。

在检查之前是否可以取消装箱(第五次检查:“取消装箱转换”)。因此,编译器检查IntegerObject的子类型,因此必须调用#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()返回ObjectStringObject,因此方法调用是正确的,并且不会引发任何异常。在第二种情况下,编译器解析对println(String)的调用,因为Box<String>,但b2.getElement()返回Integer。这不能转换为String,并且会引发ClassCastException

答案 3 :(得分:0)

没有为Integer参数定义println方法,所以你的代码将调用println(Object object),它将调用object.toString()来获取打印出来的字符串。没有类型检查,因为一切都是对象。

在第二种情况下,你的代码想要调用println(String someString),因此它将检查someString是否真的是一个String,因为它不会引发异常。