意外地将字符串添加到列表 <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));
我希望有人能告诉我编译器在执行代码时所经历的 确切 步骤,以便我能够理解输出。我目前的理解是:
- 编译器在编译期间检查 List class 中是否存在支持参数类型的 add 方法,即 add(Object e) 作为其原始类型。
- 但是,在运行时,它会尝试从实际对象 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[])
!
无论如何,不要使用原始类型或稀有类型,不要通过超载来改变行为(如果可以管理的话,也不要超载)并在提交之前真正小心对无法修复的规格的优化。
我不明白编译器在输出 Test 时如何处理以下代码,而我预计会出现错误。
List<Integer> b = new ArrayList<Integer>();
List a = b;
a.add("test");
System.out.println(b.get(0));
我希望有人能告诉我编译器在执行代码时所经历的 确切 步骤,以便我能够理解输出。我目前的理解是:
- 编译器在编译期间检查 List class 中是否存在支持参数类型的 add 方法,即 add(Object e) 作为其原始类型。
- 但是,在运行时,它会尝试从实际对象 List
调用 add(Object e) ,因为实际对象不是原始对象,所以不支持此方法-typed 而不是持有方法 add(Integer e)。
如果在实际对象 List
你很接近。编译时检查所有结果:
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[])
!
无论如何,不要使用原始类型或稀有类型,不要通过超载来改变行为(如果可以管理的话,也不要超载)并在提交之前真正小心对无法修复的规格的优化。