英文:
Why is this generic assignment illegal?
问题
我有一个类:
class Generic<T> {
List<List<T>> getList() {
return null;
}
}
当我使用通配符声明一个带有泛型通配符的 Generic
类,并调用 getList
方法时,以下赋值是非法的。
Generic<? extends Number> tt = null;
List<List<? extends Number>> list = tt.getList(); // 这一行会导致编译错误
这对我来说似乎很奇怪,因为根据 Generic
的声明,在调用 getList
时自然会创建一个 Generic<T>
并获得一个 List<List<T>>
。
实际上,它要求我编写像这样的赋值:
List<? extends List<? extends Number>> list = tt.getList(); // 这个是正确的
我想知道为什么第一个赋值是非法的,第二个赋值为什么是合法的。
我给出的示例只是一些用来说明问题的示例代码,你不必关心它们的含义。
错误消息:
不兼容的类型:
所需类型:List<java.util.List<? extends java.lang.Number>>
找到类型:List<java.util.List<capture<? extends java.lang.Number>>>
英文:
I have a class:
class Generic<T> {
List<List<T>> getList() {
return null;
}
}
When I declare a Generic
with wildcard and call getList
method, the following assignment is illegal.
Generic<? extends Number> tt = null;
List<List<? extends Number>> list = tt.getList(); // this line gives compile error
This seems odd to me because according to the declaration of Generic
, it's natural to create a Generic<T>
and get a List<List<T>>
when call getList
.
In fact, it require me to write assignment like this:
List<? extends List<? extends Number>> list = tt.getList(); // this one is correct
I want to know why the first one is illegal and why the second one is legal.
The example I give is just some sample code to illustrate the problem, you don't have to care about their meaning.
The error message:
> Incompatable types:
required : List<java.util.List<? extends java.lang.Number>>
found: List<java.util.List<capture<? extends java.lang.Number>>>
答案1
得分: 14
这是一个关于通配符类型的棘手但有趣的问题,你遇到了这个问题!当你理解了它时,它虽然棘手,但实际上是非常合乎逻辑的。
错误与通配符 ? extends Number
并不是指代一个单一的具体类型,而是指代某个未知类型有关。因此,两个 ? extend Number
的出现不一定指代相同的类型,所以编译器无法允许这种赋值。
详细解释
-
赋值中的右侧,
tt.getList()
,并不具有类型List<List<? extends Number>>
。相反,编译器会为它的每次使用分配一个唯一的生成捕获类型,例如叫做List<List<capture#1 extends Number>>
。 -
捕获类型
List<capture#1 extends Number>
是List<? extends Number>
的子类型,但并不是相同的类型!(这是为了避免混合不同的未知类型。) -
赋值的左侧类型是
List<List<? extends Number>>
。这个类型不允许List<? extends Number>
的子类型成为外部列表的元素类型,因此getList
的返回类型不能用作元素类型。 -
另一方面,类型
List<? extends List<? extends Number>>
允许List<? extends Number>
的子类型作为外部列表的元素类型。因此,这就是问题的正确修复方式。
动机
下面的示例代码演示了为什么赋值是非法的。通过一系列步骤,我们最终得到一个实际上包含了 Float
的 List<Integer>
!
class Generic<T> {
private List<List<T>> list = new ArrayList<>();
public List<List<T>> getList() {
return list;
}
}
// 从一个具体类型开始,稍后会被破坏
Generic<Integer> genInt = new Generic<>();
// 向 genInt.list 添加一个 List<Integer>。这对于主要示例来说并不是必需的,
// 但可以使事情变得更清晰一些。
List<Integer> ints = List.of(1);
genInt.getList().add(ints);
// 像问题中的通配符类型一样赋值
Generic<? extends Number> genWild = genInt;
// 非法赋值。通常情况下,这不会编译,但我们使用不受检查的强制转换来强制编译,
// 以查看如果编译了会发生什么。
List<List<? extends Number>> list =
(List<List<? extends Number>>) (Object) genWild.getList();
// 这是关键的一步:
// 向 List<List<? extends Number>> 添加一个 List<Float> 是合法的。
// list 引用 genInt.list,它的类型是 List<List<Integer>>。
// 发生了堆污染!
List<Float> floats = List.of(1.0f);
list.add(floats);
// notInts 实际上与 floats 是同一个列表!
List<Integer> notInts = genInt.getList().get(1);
// 这个语句从 List<Integer> 读取一个 Float。会抛出 ClassCastException。
// 编译器必须不允许我们在没有任何先前类型错误或不受检查的转换警告的情况下
// 进入这里。
Integer i = notInts.get(0);
你发现的修复方法是使用以下类型来定义 list
:
List<? extends List<? extends Number>> list = tt.getList();
这个新的类型将类型错误从对 list
的赋值移到了对 list.add(...)
的调用上。
上述示例说明了通配符类型的全部目的:在不混合类型并得到意外的 ClassCastException
的情况下,跟踪何时可以安全地读取和写入值。
一般经验法则
对于像这样存在嵌套带有通配符的类型参数的情况,有一个一般经验法则:
> 如果内部类型中有通配符,那么外部类型通常也需要有通配符。
否则,内部通配符不能“生效”,就像你所看到的那样。
参考资料
Java 教程 中包含了关于捕获类型的一些信息。
这个问题有一些关于通配符的一般信息的答案:
https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super
英文:
This is a tricky but interesting thing about wildcard types that you have run into! It is tricky but really logical when you understand it.
The error has to do with the fact that the wildcard ? extends Number
does not refer to one single concrete type, but to some unknown type. Thus two occurrences of ? extend Number
don't necessarily refer to the same type, so the compiler can't allow the assignment.
Detailed explanation
-
The right-hand-side in the assignment,
tt.getList()
, does not get the typeList<List<? extends Number>>
. Instead each use of it is assigned by the compiler a unique generated capture type, for exampled calledList<List<capture#1 extends Number>>
. -
The capture type
List<capture#1 extends Number>
is a subtype ofList<? extends Number>
, but it is not type same type! (This is to avoid mixing different unknown types together.) -
The type of the left-hand-side in the assignment is
List<List<? extends Number>>
. This type does not allow subtypes ofList<? extends Number>
to be the element type of the outer list, thus the return type ofgetList
can't be used as the element type. -
The type
List<? extends List<? extends Number>>
on the other hand does allow subtypes ofList<? extends Number>
as the element type of the outer list. So that is the right fix for the problem.
Motivation
The following example code demonstrates why the assignment is illegal. Through a sequence of steps we end up with a List<Integer>
which actually contains Float
s!
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);
The fix that you discovered was to use the following type for list
:
List<? extends List<? extends Number>> list = tt.getList();
This new type shifts the type error from the assignment of list
to the call to list.add(...)
.
The above illustrates the whole point of wildcard types: To keep track of where it is safe to read and write values without mixing up types and getting unexpected ClassCastException
s.
General rule of thumb
There is a general rule of thumb for situations like this, when you have nested type arguments with wildcards:
> If the inner types have wildcards in them, then the outer types often need wildcards also.
Otherwise the inner wildcard can't "take effect", in the way you have seen.
References
The Java Tutorial contains some information about capture types.
This question has answers with general information about wildcards:
https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论