意外地将字符串添加到列表 <Integers>

Unexpected adding String to List<Integers>

我不明白编译器在输出 Test 时如何处理以下代码,而我预计会出现错误。

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

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

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

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

你很接近。编译时检查所有结果:

a 是类型 List 所以调用

a.add("test");

成功了。 b 是(编译时)类型 ArrayList<Integer> 所以

b.get(0)

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

现在您可能会假设在 运行 时可以检查此类内容。 las,在 Java 他们不能。 Java 擦除通用类型。查找有关 Java 类型擦除的文章,了解详细信息。 TL;DR 是编译时的 List<Integer> 在 运行 时变成原始的 List。 JVM 没有办法 "reify" 泛型(尽管其他语言可以!)所以当引入泛型时,决定 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

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

令人惊讶的是 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[])

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