Java - 获取泛型对象作为 String 泛型类型抛出异常

Java - Obtaining generic object as String Generic type throws exception

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 类型的元素。在情况 1 中,我将第一个元素作为 Box<Integer> 获取,在第二种情况下,列表中的第二个元素作为 Box<String> 获取。在第一种情况下不会抛出 ClassCastException。

当我尝试调试时,b1b2 中的 element's 类型分别是 StringInteger

是否与类型擦除有关?

Ideone link

在编译时,编译器知道类型并将 System.out.println(..) 的调用链接到具有正确参数类型的方法。在第一种情况下,编译器解析对 println(Object) 的调用。因为 b1.getElement() returns 一个 ObjectString 是一个 Object,方法调用是正确的,没有引发异常。在第二种情况下,编译器解析对 println(String) 的调用,因为 Box<String>,但是 b2.getElement() returns 和 Integer。这不能转换为 String 并引发 ClassCastException

好的,这里的问题是 b2 错误地 标记为 Box<String> 而实际上它是 Box<Integer> (box2 的类型) - 所以 b2.getElement() 被键入为字符串,即使它实际上包含一个整数。编译器尝试调用采用 String 的重载 println 方法,而不是采用 Object 的方法,因此您会得到 ClassCastException。 println 的对象版本将其参数显式转换为字符串(通过调用 toString()),但该方法的字符串版本不这样做。

潜在的问题是使用原始类型而不是完全指定列表 l 的类型参数 - 它应该是 List<Box<?>>。然后你会有 b1b2 作为 Box 并且会选择 System.out.println 的正确重载。

准确的说,问题是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 的强制转换,因为这里没有必要。编译器已经将使用的重载方法链接到 PrintStream#(Object)。 它这样做是由于 JLS rule §5.3:

Method invocation conversion is applied to each argument value in a method or constructor invocation (§8.8.7.1, §15.9, §15.12): the type of the argument expression must be converted to the type of the corresponding parameter.

Method invocation contexts allow the use of one of the following:

  • an identity conversion (§5.1.1)
  • a widening primitive conversion (§5.1.2)
  • a widening reference conversion (§5.1.5)
  • a boxing conversion (§5.1.7) optionally followed by widening reference conversion
  • an unboxing conversion (§5.1.8) optionally followed by a widening primitive conversion.

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

A widening reference conversion exists from any reference type S to any reference type T, provided S is a subtype (§4.10) of 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.

未为 Integer 参数定义 println 方法,因此您的代码将调用 println(Object object),后者将调用 object.toString() 以获取需要打印的字符串。没有类型检查,因为一切都是对象。

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