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

hao*_*ang 10 java generics bounded-wildcard

我有一堂课:

class Generic<T> {
    List<List<T>> getList() {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

这对我来说似乎很奇怪,因为根据 的声明Generic,很自然地创建 aGeneric<T>并获得 a List<List<T>>when call getList

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

List<? extends List<? extends Number>> list = tt.getList(); // this one is correct
Run Code Online (Sandbox Code Playgroud)

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

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

错误信息:

不兼容的类型:
必需:List<java.util.List<? extends java.lang.Number>>
找到:List<java.util.List<capture<? extends java.lang.Number>>>

Lii*_*Lii 14

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

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

详细说明

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

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

  3. 赋值中左侧的类型是List<List<? extends Number>>。此类型不允许 的子List<? extends Number>类型作为外部列表的元素类型,因此getList不能将的返回类型用作元素类型。

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

动机

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

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);
Run Code Online (Sandbox Code Playgroud)

您发现的修复方法是将以下类型用于list

List<? extends List<? extends Number>> list = tt.getList();
Run Code Online (Sandbox Code Playgroud)

这种新类型将类型错误从 的分配转移list到对 的调用list.add(...)

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

一般经验法则

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

如果内部类型中有通配符,那么外部类型通常也需要通配符。

否则内部通配符无法按照您所见的方式“生效”。

参考

Java教程包含有关捕获类型的一些信息。

这个问题有关于通配符的一般信息的答案:

什么是 PECS(生产者扩展消费者超级)?