【问题标题】:Unexpected adding String to List<Integers>意外将字符串添加到 List<Integers>
【发布时间】:2018-10-06 16:31:14
【问题描述】:

我不明白编译器如何处理以下代码,因为它输出 Test 而我期待一个错误。

List<Integer> b = new ArrayList<Integer>();
List a = b;
a.add("test");
System.out.println(b.get(0));

我希望有人能告诉我编译器在执行代码时经过的确切步骤,以便我理解输出。我目前的理解是:

  1. 编译器会在编译期间检查 List 类中是否存在支持参数类型的 add 方法,该类是 add(Object e) 作为其原始类型。
  2. 但是,在运行时,它会尝试从实际对象 List 调用 add(Object e),该对象不包含此方法,因为实际对象不是原始类型,而是包含add(Integer e) 方法。

如果实际对象 List 中没有 add(Object e) 方法,它如何仍然以某种方式将字符串添加到整数列表中?

【问题讨论】:

  • 列表不执行类型检查。毕竟这只是一个引用数组,所以是的,它会工作,但它是不安全的!
  • a 列表是原始的,这意味着它存储了 Object 引用。因此,您可以向其中添加任何 Java 类,因为所有内容都隐式扩展了 Object。如果您尝试将String 添加到b 列表中,该错误就会起作用。这会在编译时失败,因为泛型会阻止这种情况发生。
  • 很好的问题,你很接近。您的第 1 项是正确的,但 Java 有一个名为 type erasure 的东西,因此您的第 2 项实际上并不适用。
  • 如果实际对象 List 中没有 add(Object e) 方法,那你就搞错了。这是一个List,在运行时它是一个List&lt;Object&gt;,所以一个add(Object)。编译器插入一个类型转换,这就是失败的原因。
  • 泛型是编译时检查,通过List a = b,您将关闭编译时检查。

标签: java generics collections polymorphism raw-types


【解决方案1】:

你很接近。编译时检查一切正常:

aList 类型,所以调用

a.add("test");

成功了。 b 是(编译时)类型 ArrayList&lt;Integer&gt; 所以

b.get(0)

也检查出来。请注意,仅针对变量的编译时类型进行检查。当编译器看到a.add("test") 时,它确实知道变量a 引用的对象的运行时值。一般来说,它确实不能(理论计算机科学对此有一个结果),尽管控制流类型分析可以捕获许多这样的东西。像 TypeScript 这样的语言可以在编译时做出惊人的事情。

现在您可能会假设在运行时可以检查这些事情。唉,在 Java 中他们不能。 Java 删除泛型类型。查找有关 Java 类型擦除的文章以了解详细信息。 TL;DR 是编译时的 List&lt;Integer&gt; 在运行时变成原始的 List。 JVM 没有办法“具体化”泛型(尽管其他语言可以!)所以当引入泛型时,Java 决定只删除泛型类型。所以在运行时,您的代码中没有类型问题。

我们看一下编译后的代码:

   0: new           #2                  // class java/util/ArrayList
   3: dup
   4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
   7: astore_1
   8: aload_1
   9: astore_2
  10: aload_2
  11: ldc           #4                  // String test
  13: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  18: pop
  19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  22: aload_1
  23: iconst_0
  24: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  29: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  32: return

这里可以直接看到没有运行时类型检查。因此,对您的问题的完整(但看似轻率)的答案是 Java 仅在编译时根据 variables 的类型(在编译时已知)检查类型,但泛型类型参数被删除并且代码在没有它们的情况下运行。

【讨论】:

  • 感谢您的评论。我仍然没有 100% 理解它,因为我需要看一下 Java 编译器处理对象编译时间和运行时间的方式。一旦我完全理解了这一点,我敢打赌你的帖子对我来说会更有意义。类型擦除也很有帮助。
  • 同意这是不直观的。要理解的最重要的部分是,当编译器看到a.add(test) 时,编译器只会询问a 的声明类型是什么? 因为a 被声明为List,所以编译器会这样说好的。即使a 引用的objectList&lt;Integer&gt;,编译器通常也不知道这一点。它仅根据声明的类型对表达式进行类型检查,而不是根据运行时将存在的值。 “类型擦除”解释了程序运行的原因,但至于它为什么编译,原因是 a声明类型为 List。学习愉快!
【解决方案2】:

这里的惊喜是b.get(0) 没有运行时检查。我们希望编译器将代码解释为:

System.out.println((Integer)b.get(0)); // throws CCE

确实,如果我们要尝试:

Integer str = b.get(0); // throws CCE

我们会得到一个运行时ClassCastException

事实上,我们甚至会在切换printf 代替println 时遇到同样的错误:

System.out.printf(b.get(0)); // throws CCE

这有什么意义?

这是一个由于向后兼容性而无法修复的错误。如果目标上下文可以允许删除检查强制转换,那么尽管改变了语义,它还是会被忽略。在这种情况下,重载从println(Integer) 变为println(Object)。比这更糟糕的是,println(char[]) 有一个具有不同行为的重载!

无论如何,不​​要使用原始类型或稀有类型,不要重载来改变行为(或者如果可以管理的话,不要重载)并在将优化提交给无法修复的问题之前要真正小心规格。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多