22

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

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

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

它知道,例如,通过执行带有IntegerList 的方法,它会从i.get一个Integer值中获取。因此它尝试将Integer索引0处的值设置为相同的整数列表(i)。

那么,怎么了?为什么要写通配符助手?

4

7 回答 7

25

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

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

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

指的是相同的实际类型?

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

于 2012-08-20T19:59:20.297 回答
23

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

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

它知道,例如,通过执行带有 Integer List 的方法,它会从 i.get 获取一个 Integer 值。

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

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

更多阅读:

于 2012-08-20T19:32:46.077 回答
2

根据Get-Put原理:

  1. 如果您在 中具有扩展通配符List<? extends Something>,则:

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

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

    1B。您不能向结构中添加任何内容(除了null)。

    void foo(List<? extends Number> nums) {
       nums.add(1); Compile error
       nums.add(1L); Compile error
       nums.add(null); // only null is allowed.
    }
    
  2. 同样,如果你有超级通配符List<? super Something>,那么:

    2A。您可以添加到结构 which isSomething或其subclass. 例如:

    void foo(List<? super Number> 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<? super Integer> 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<? extends Object> i. 而且由于它是extends通配符,因此set操作失败。

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

于 2018-08-11T19:29:11.377 回答
1

解决方案将是,

import java.util.List;

    public class WildcardError {

    private void fooHelper(List<T> i){
         i.set(0, i.get(0));
    }
    public void foo(List<?> i){
         fooHelper(i);
    }
}

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

于 2018-07-13T13:26:28.223 回答
1

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

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

那就是——为了理解这个问题——与制作一个局部变量相同,为方便起见,我给出了这个名称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 时,它只能将类型设置elementObject(--> 上限概念,参见马特的回答),因为它不能?用作变量类型;仅用于?指示泛型类型未知。

变量类型只能是真实类型或泛型类型,但由于您在此方法中不使用泛型类型,<T>例如,它被迫使用真实类型。由于 java 规范 (jls8, 18.2.1) 中的以下行,执行此强制:

‹Expression → T›形式的约束公式简化如下:

[...]

— 如果表达式是类实例创建表达式或方法调用表达式,则约束减少到绑定集 B3,当以 T 为目标时,约束集将用于确定表达式的调用类型,如第 18.5.2 节中定义的那样。(对于类实例创建表达式,用于推理的相应“方法”在 §15.9.3 中定义)。

于 2015-07-23T15:59:04.977 回答
0

我想你误解了?泛型。它不是运行时的“通配符捕获”。它是编译时的“通配符捕获”。由于该捕获在编译后消失了,因此List<?>通常会成为List<Object>某个接口或公共基类的列表,这些接口或公共基类是根据泛型类型在实例之间共享的最低子类。

因为 Java 必须使泛型系统与支持 JVM 的非泛型系统兼容,所以在运行时没有泛型类型。这意味着在运行时不存在所有检查/捕获。编译器在编译时允许非常窄范围的兼容泛型类型分配,或者抛出错误。

现在,如果您想在运行时保存一个泛型类型,有一些方法。其中大多数涉及将 Class 对象作为参数传递给构造函数,构造函数将其保存在字段中。然后您可以参考该字段以获取确切传递的类型。您可以将此类型与您的泛型参数纠缠在一起;并且通过这样做,您可以在运行时代码中的“泛型类型参数”已被真实 Java 类型系统支持的最宽松类型替换的地方拥有实际类型。我不建议在很多情况下这样做,因为它需要做很多工作(而且不太可能给你更多的安全性);但是,有时你确实需要这种类型。

于 2021-12-13T14:14:15.187 回答
0

我猜你对限制的误解来自于替换?for any typeObject或者你脑海中的类似东西。但这种假设并不正确,?实际上意味着unknown type。所以在下面一行

fooz.set(0, foo);

您正在尝试将某种类型的变量分配给未知类型的变量(因为函数签名就像void set(int, ?)),无论是哪种类型,foo这都是不可能的。在您的情况下, foo 的类型是Object并且您不能将其分配给某种未知类型的变量,实际上可能是FooBar或任何其他类型。

于 2019-08-07T05:32:34.697 回答