为什么这个通用赋值是非法的?

Why is this generic assignment illegal?

我有一个class:

class Generic<T> {
    List<List<T>> getList() {
        return null;
    }
}

当我用通配符声明 Generic 并调用 getList 方法时,以下赋值是非法的。

Generic<? extends Number> tt = null;
List<List<? extends Number>> list = tt.getList(); // this line gives compile error

这对我来说似乎很奇怪,因为根据 Generic 的声明,在调用 getList 时创建一个 Generic<T> 并得到一个 List<List<T>> 是很自然的。

事实上,它要求我这样写作业:

List<? extends List<? extends Number>> list = tt.getList(); // this one is correct

我想知道为什么第一个是非法的,为什么第二个是合法的。

我给出的例子只是一些示例代码来说明问题,你不必关心它们的含义。

错误信息:

Incompatable types:
required : List<java.util.List<? extends java.lang.Number>>
found: List<java.util.List<capture<? extends java.lang.Number>>>

关于您 运行 的通配符类型,这是一件棘手但有趣的事情!这很棘手,但当你理解它时确实合乎逻辑。

错误与以下事实有关:通配符 ? extends Number 不是指一个单一的具体类型,而是指某个未知类型。因此两次出现的 ? extend Number 不一定指的是同一类型,因此编译器不允许赋值。

详细解释

  1. 赋值中的right-hand-side,tt.getList(),没有得到类型List<List<? extends Number>>。相反,它的每次使用都由编译器分配一个唯一生成的捕获类型,例如称为 List<List<capture#1 extends Number>>.

  2. 捕获类型List<capture#1 extends Number>List<? extends Number>的子类型,但不是同一类型! (这是为了避免将不同的未知类型混合在一起。)

  3. 赋值中left-hand-side的类型是List<List<? extends Number>>。此类型不允许 List<? extends Number> 的子类型作为外部列表的元素类型,因此 getList 的 return 类型不能用作元素类型。

  4. 另一方面,类型List<? extends List<? extends Number>>确实允许List<? extends Number>的子类型作为外部列表的元素类型。所以这是解决问题的正确方法。

动机

下面的示例代码演示了为什么赋值是非法的。通过一系列步骤,我们最终得到一个 List<Integer>,它实际上包含 Floats!

class Generic<T> {
    private List<List<T>> list = new ArrayList<>();

    public List<List<T>> getList() {
        return list;
    }
}

// Start with a concrete type, which will get corrupted later on
Generic<Integer> genInt = new Generic<>();

// Add a List<Integer> to genInt.list. This is not necessary for the
// main example but migh make things a little clearer.
List<Integer> ints = List.of(1);
genInt.getList().add(ints); 

// Assign to a wildcard type as in the question
Generic<? extends Number> genWild = genInt;

// The illegal assignment. This doesn't compile normally, but we force it
// using an unchecked cast to see what would happen IF it did compile.
List<List<? extends Number>> list =
    (List<List<? extends Number>>) (Object) genWild.getList();

// This is the crucial step: 
// It is legal to add a List<Float> to List<List<? extends Number>>.
// list refers to genInt.list, which has type List<List<Integer>>.
// Heap pollution occurs!
List<Float> floats = List.of(1.0f);
list.add(floats);

// notInts in reality is the same list as floats!
List<Integer> notInts = genInt.getList().get(1);

// This statement reads a Float from a List<Integer>. A ClassCastException
// is thrown. The compiler must not allow us to end up here without any
// previous type errors or unchecked cast warnings.
Integer i = notInts.get(0);

您发现的修复方法是对 list 使用以下类型:

List<? extends List<? extends Number>> list = tt.getList();

这个新类型将类型错误从 list 的赋值转移到对 list.add(...).

的调用

以上说明了通配符类型的全部要点:跟踪可以安全读取和写入值的位置,而不会混淆类型和出现意外 ClassCastExceptions。

一般经验法则

对于这种情况,当您使用通配符嵌套类型参数时,有一个一般的经验法则:

If the inner types have wildcards in them, then the outer types often need wildcards also.

否则内部通配符不能像你看到的那样“生效”。

参考资料

Java Tutorial 包含一些关于捕获类型的信息。

这个问题的答案包含通配符的一般信息:

What is PECS (Producer Extends Consumer Super)?