为什么这个涉及通配符的赋值在 Java 中是合法的?

Why is this assignment involving wildcards legal in Java?

大多数关于通配符的问题都想知道为什么一些合理的东西会被编译器拒绝。我的问题恰恰相反。为什么下面的程序能被编译器接受?

void test(List<? extends Number> g1, List<? extends Number> g2)
{
    g1 = g2;
}

我试图从 Java 语言规范中解释这一点,但我没有找到答案。我从对 Java 泛型和通配符的各种描述中得到的印象是,通配符的每次使用都被捕获为一个全新的类型,但显然不是在这里。我没有发现允许此分配的任何令人讨厌的行为,但它似乎仍然是“错误的”。

List<? extends Number> 最好读作:

这是一个数字列表,但是,是协变的。

换句话说,这是一个具体但未知类型的列表。但是,我确实知道,无论它是什么类型,至少它是 Number 或其某个子类。

泛型很奇怪;一旦你选择了一些差异,你就会得到相应的限制。在collections的情况下,'covariance'带有'no adding'的行李。

试一试。

g1.add(XXX);

这里唯一对 XXX 合法的东西是什么? null。就是这样。你可以添加到这个东西的所有内容的完整、完整和详尽的列表。当然 Number x = 5; g1.add(x); 不会被 javac 允许。

通过写 List<? extends a thingie> 你是在说:是的,我想要那个。我签署了这个限制,我绝对不能添加任何东西(除了文字 null 的学术案例)。为了给自己戴上手铐,你可以为 g1 传递的东西大大扩展了。

您也可以选择逆变:

void foo(List<? super Integer> list) {
    list.add(Integer.valueOf(5)); // works!
    Integer x = list.get(0); // no go
}

逆变是相反的。添加作品。获取不起作用。在这种情况下意味着:表达式 list.get(0) 的类型只是.. Object.


现在我们已经介绍过了:

void test(List<? extends Number> g1, List<? extends Number> g2) {}

表示 'my first parameter is a list of numbers, but I opt into covariance handcuffs',而 'my second parameter is a list of numbers, but I also opt into covariance handcuffs for this one too',现在可以理解为什么 java 让你写 g1 = g2。 g2 保证是 X<Y>,其中 X 是 List 的某个具体子类,Y 是 Number 或其某个子类。

这与 'some sort of list whose type param is some covariant take on Number' 的概念 100% 兼容,type-wise。您唯一可以做的 List<? extends Number> 是调用 List 的方法,其中签名中的任何 T 对于参数都是 'disabled',并替换为绑定(Number) 对于 return类型。

那是..正是List<? extends Number>所描述的,所以它是兼容的。

“我对 Java 泛型和通配符的各种描述给我的印象是,通配符的每次使用都被捕获为一个全新的类型,”

这个说法是正确的。

那又怎样?您混淆了对象的类型和变量的类型。

考虑这段代码:

String s = "abc";
Object o = s;

o 的类型是 Object,它的赋值与 s 的类型兼容。但这并不意味着 String 和 Object 是同一类型。与您的示例没有什么不同。对象有两种不同的列表类型,变量有一种类型。每个变量的类型都是 List,所以分配没问题。当您进行赋值时,对象的泛型类型是 List,表示一些全新的未知类型 x。但是变量类型仍然是 List.

怎么可能有效?

两个变量具有相同的类型(在本例中 List<? extends Number>),因此编译器 必须 允许将一个变量赋值给另一个变量。


赋值给变量的对象可能有不同的类型,但是变量类型是相同的,所以赋值总是合法的。

编译器不知道或不关心分配给变量的对象的实际类型是什么,即使它可以从代码中确定。在检查类型时它只关心声明的类型。

当我面对这些问题时,我的处理方式略有不同。

首先,每个 wildcard 都是 capturedeverywhere,由 javac。用简单的英语来说:每次 javac “看到”一个 wildcard 都会对其进行转换(这是 几乎 准确的,正如您将进一步看到的那样)。具体来说,假设我们有这个:

List<? extends Number> list;

javac 将转换为:

List<X1> list

其中 X1 <: Number,其中 <: 表示它是 的 子类型,因此:X1 is an unknown type that extends Number。每次出现都会发生这种情况。在某些情况下,一开始可能会很奇怪:

public static void main(String[] args) {
    List<?> l = new ArrayList<String>();
    one(l);
    two(l, l); // fails
}

public static <T> void one(List<T> single){

}

public static <T> void two(List<T> left, List<T> right){

}

捕获转换被单独应用于每个List,就像这样发生了:

two(List<X1>, List<X2>)

现在为什么你的例子被接受,更有趣,恕我直言。您知道应用了 捕获转换 ,但根据 JLS 它是 not applied everywhere:

If the expression name is a variable that appears "on the left hand side", its type is not subject to capture conversion.

这就像说只有被捕获转换,而不是变量

所以在这种情况下:

g1 = g2;

g1 没有被捕获转换,而 g2 有。就像做:

List<? extends Number> g1 = List<X1> (g2) // pseudo-code

我们知道 X1 <: Number 因此,List<X1>List<? extends Number> 的子类型,因此赋值有效。

即使您将 ? extends Number 更改为 ?(这不再是有界通配符),这仍然有效。