意外地将String添加到List <integers>

时间:2018-10-06 16:31:14

标签: java generics collections polymorphism raw-types

我不知道编译器如何处理以下代码,因为在我期望出现错误时它会输出 Test

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

我希望有人可以告诉我编译器执行代码时执行的 exact 步骤,这样我才能理解输出。我目前的理解是:

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

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

2 个答案:

答案 0 :(得分:4)

您非常亲密。编译时检查所有结果:

a的类型为List,因此通话

a.add("test");

显示。 b的类型(编译时)为ArrayList<Integer>,因此

b.get(0)

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

现在,您可能假设可以在运行时检查此类情况。 Java,在Java中,它们不能。 Java擦除通用类型。查找有关Java类型擦除的文章,了解更多细节。 TL; DR是在编译时将List<Integer>变成运行时的原始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仅在编译时根据变量的类型(在编译时已知)检查类型,但是通用类型参数被删除并代码在没有它们的情况下运行。

答案 1 :(得分:0)

这里令人惊讶的是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[])的重载具有不同的行为!

无论如何,请不要使用原始或稀有类型,也不要重载来更改行为(如果可以管理,也不要重载),并且在对不可修复的优化进行优化之前要认真对待。规格。