【问题标题】:Java - Obtaining generic object as String Generic type throws exceptionJava - 将泛型对象获取为字符串泛型类型会引发异常
【发布时间】:2016-03-26 08:46:08
【问题描述】:
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&lt;Integer&gt;,在第二种情况下,列表中的第二个元素作为Box&lt;String&gt; 获得。在第一种情况下不会抛出 ClassCastException。

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

和类型擦除有关吗?

Ideone link

【问题讨论】:

  • 我的猜测是 b1 在运行时被认为是 Box&lt;Object&gt; 但这确实是一种奇怪的行为。

标签: java generics type-erasure generic-collections


【解决方案1】:

准确来说,问题是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:

方法调用转换应用于方法或构造函数调用中的每个参数值(§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 的扩展引用转换。

并且是在检查该类型是否可以拆箱之前完成的(第五次检查:"an unboxing conversion")。所以编译器检查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&lt;String&gt; b2 = l.get(1);)并且它链接了方法PrintStream#(String)。此检查因提到的 ClassCastException 而失败,因为无法将 Integer 强制转换为 String

【讨论】:

    【解决方案2】:

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

    根本问题是使用原始类型而不是完全指定列表l 的类型参数——它应该是List&lt;Box&lt;?&gt;&gt;。那么您将拥有 b1b2 作为 Box,并且将选择 System.out.println 的正确重载。

    【讨论】:

    • 那么我尝试调用b1.getElement()时没有出现这个问题的原因是调用了带有Object参数的println?
    • 是的,b1 也被错误地输入了,但在运行时它并不重要,因为它被强制转换为 Object。
    【解决方案3】:

    在编译时,编译器知道类型并将System.out.println(..) 的调用链接到具有正确参数类型的方法。在第一种情况下,编译器将调用解析为println(Object)。因为b1.getElement()返回一个ObjectString是一个Object,所以方法调用是正确的,不会引发异常。在第二种情况下,由于Box&lt;String&gt;,编译器将调用解析为println(String),但b2.getElement() 返回Integer。这不能转换为 String 并且会引发 ClassCastException

    【讨论】:

    • 您认为 system.out.println(10) 不起作用。
    • b.getElement() 返回一个Integer。我认为您的意思是调用以 Object 作为参数的 println 版本。
    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-08
      • 1970-01-01
      • 2012-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-29
      • 2015-06-08
      相关资源
      最近更新 更多