Java Generics:通配符捕获误解

阅读Java在线教程我对通配符捕获一无所知。 例如:

import java.util.List; public class WildcardError { void foo(List i) { i.set(0, i.get(0)); } } 

为什么编译器不能保持赋值安全? 它知道,通过执行例如带有整数列表的方法,它从i.get得到一个Integer值。 因此,它尝试将索引0处的Integer值设置为相同的整数列表(i)。 那么,出了什么问题?为什么要写Wildcard助手?

为什么编译器不能保持赋值安全?

根据定义,编译器对List i的元素类型一无所知? 。 通配符并不意味着“任何类型”; 它意味着“某种未知类型”。

它知道,通过执行例如带有整数列表的方法,它从i.get得到一个Integer值。

这是真的,但正如我上面所说:编译器只能知道 – 在编译时 ,记住 – i.get(0)返回一个Object ,它是?的上限? 。 但是不能保证? 在运行时是 Object ,因此编译器无法知道i.set(0, i.get(0))是一个安全的调用。 这就像写这个:

 List fooz = /* init */; Object foo = fooz.get(0); fooz.set(0, foo); // won't compile because foo is an object, not a Foo 

更多阅读:

  • 无法使用通配符generics类型为Java集合添加值
  • 使用通配符的Java集合
  • java中的通用集合和通配符
  • generics – 无法添加到带有无界通配符的列表
  • Collection <?>和Collection 之间有什么区别

为什么编译器不能保持赋值安全? 它知道,通过执行例如带有整数列表的方法,它从i.get得到一个Integer值。 因此,它尝试将索引0处的Integer值设置为相同的整数列表(i)。

换句话说,为什么编译器不知道通配符类型List中的两个用法

 i.set(0, i.get(0)); 

参考相同的实际类型?

那么,这将要求编译器知道i对表达式的两个评估都包含相同的实例。 由于i甚至不是最终的,编译器必须检查是否可能在评估两个表达式之间分配了i 。 这样的分析只对局部变量很简单(谁知道被调用的方法是否会更新特定对象的特定字段?)。 这在编译器中很少有额外的复杂性,很少显示出好处。 我想这就是为什么Java编程语言的设计者通过指定相同通配符类型的不同用法具有不同的捕获来使事情变得简单。

我也觉得这个问题很难理解; 将单个命令分成两个命令对我有帮助。

下面的代码是在检查和编译原始方法时后台实际发生的情况,编译器创建自己的局部变量: i.get(0)调用的结果放在局部变量堆栈的寄存器中。

那就是 – 为了理解这个问题 – 就像制作一个局部变量一样,为方便起见我给了name element

 import java.util.List; public class WildcardError { void foo(List i) { Object element = i.get(0); // command 1 i.set(0, element); // command 2 } } 

当检查命令1时,它只能将element的类型设置为Object ( – > upperbound concept,参见Matt的答案),因为它不能使用? 作为变量类型; 的? 仅用于指示generics类型未知。

变量类型只能是实数类型或generics类型,但由于您不在此方法中使用generics类型(例如 ,因此它必须使用实数类型。 由于java规范中的以下行(jls8,18.2.1),因此执行此强制:

forms的约束公式如下:

[…]

– 如果表达式是类实例创建表达式或方法调用表达式,则约束将缩减为绑定集B3,用于在定位T时确定表达式的调用类型,如第18.5.2节中所定义。 (对于类实例创建表达式,用于推理的相应“方法”在第15.9.3节中定义)。

解决方案将是,

 import java.util.List; public class WildcardError { private void fooHelper(List i){ i.set(0, i.get(0)); } public void foo(List i){ fooHelper(i); } } 

这里fooHelper将捕获通配符的类型T? (如名称通配符捕获)。

根据Get-Put原则:

  1. 如果您在List扩展了通配符List List ,然后:

    1A。 您可以使用Something或其superclass引用从结构中获取。

     void foo(List nums) { Number number = nums.get(0); Object number = nums.get(0); // superclass reference also works. } 

    1B。 您无法向结构添加任何内容( null除外)。

     void foo(List nums) { nums.add(1); Compile error nums.add(1L); Compile error nums.add(null); // only null is allowed. } 
  2. 同样,如果你有List超级通配符List List ,然后:

    2A。 您可以添加到Something或其subclass的结构。 例如:

     void foo(List nums) { nums.add(1); // Integer is a subclass of Number nums.add(1L); // Long is a subclass of Number nums.add("str"); // Compile error: String is not subclass of Number } 

    2A。 您无法从结构中获取(通过对象引用除外)。 例如:

     void foo(List nums) { Integer num = nums.get(0); // Compile error Number num = nums.get(0); // Compile error Object num = nums.get(0); // Only get via Object reference is allowed. } 

回到OP的问题, List i只是List i的简短表示List i List i 。 由于它是extends通配符,因此set操作失败。

剩下的最后一块是为什么操作失败了? 或者为什么Get-Put原则首先? – 这与Jon Skeet 在这里回答的类型安全有关。