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

huangapple go评论76阅读模式
英文:

Why is this assignment involving wildcards legal in Java?

问题

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

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

我试图从Java语言规范中解释这个问题,但我没有找到答案。从各种关于Java泛型和通配符的描述中,我对每个通配符的使用被捕获为一个全新的类型有印象,但显然在这里不是这样。我没有找到任何因允许这种赋值而产生的不良行为,但它仍然看起来有点“错误”。

英文:

Most questions about wildcards want to know why something sensible is rejected by the compiler. My question is the opposite. Why is the following program accepted by the compiler?

void test(List&lt;? extends Number&gt; g1, List&lt;? extends Number&gt; g2)
{
    g1 = g2;
}

I tried to explain this from the Java Language Specification, but I have not found the answer. I had the impression from various descriptions of Java generics and wildcards that each use of a wildcard is captured as a completely new type, but apparently not here. I have not found any nasty behavior that follows from this assignment being allowed, but it still seems "wrong".

答案1

得分: 4

当我面对这些问题时,我会以稍微不同的方式来处理。

首先,每一个通配符都会被javac捕获,无论在哪里。用简单的英语来说:每当javac“看到”一个通配符时,它都会对其进行转换(正如你将在后面看到的,这几乎是准确的)。具体来说,假设我们有这样一个例子:

List<? extends Number> list;

javac会转换为:

List<X1> list

其中 X1 <: Number,其中<:表示它是一个"子类型",因此:X1是一个未知类型,它是Number的子类型。对于每个出现的情况都会这样处理。在某些情况下,这可能会很奇怪:

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

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的规定,它不是在所有情况下都适用

如果表达式名称是出现在“左侧”的变量,则其类型不受捕获转换的影响。

这就像是在说只有"值"会被捕获转换,而不是"变量"。

因此在这种情况下:

g1 = g2;

g1没有被捕获转换,而g2被捕获转换了。就像是在执行:

List<? extends Number> g1 = List<X1> (g2) // 伪代码

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

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

英文:

When I face these questions, I approach this in a slightly different manner.

First of all, every single wildcard is captured, everywhere, by javac. In plain english: every time javac "sees" a wildcard it is going to transform that (this is almost accurate as you will see further). Specifically, let's say we have this:

List&lt;? extends Number&gt; list;

javac will transform to:

List&lt;X1&gt; list

where X1 &lt;: Number, where &lt;: means it is a subtype of, as such : X1 is an unknown type that extends Number. This will happen for every single occurrence. And it might be very weird, at first, in some scenarios:

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

public static &lt;T&gt; void one(List&lt;T&gt; single){

}

public static &lt;T&gt; void two(List&lt;T&gt; left, List&lt;T&gt; right){

}

capture conversion was applied individually to each List, it's like this happened:

two(List&lt;X1&gt;, List&lt;X2&gt;)

Now to why is your example accepted, is far more interesting, imho. You know that capture conversion is applied, but according to the JLS it is 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.

It's like saying that only values are capture converted, not variables.

So in this case:

g1 = g2;

g1 has not been capture converted, while g2 has. It's like doing:

List&lt;? extends Number&gt; g1 = List&lt;X1&gt; (g2) // pseudo-code

We know that X1 &lt;: Number so, as such List&lt;X1&gt; is a subtype of List&lt;? extends Number&gt;, so the assignment works.

Even if you change ? extends Number to ? (this is not a bounded wildcard anymore), this would still work.

答案2

得分: 3

List<? extends Number>最好理解为:

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

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

泛型很奇怪;一旦你选择了某种变化,你就会得到相应的限制。在集合的情况下,“协变”伴随着“无法添加”的限制。

试试看。

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)); // 可行!
    Integer x = list.get(0); // 不行
}

逆变正好相反。添加是可行的,获取不可行。在这种情况下,这意味着表达式list.get(0)的类型是Object


既然我们已经涵盖了这个:

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

意味着“我的第一个参数是一个数字列表,但我选择协变限制”,以及“我的第二个参数也是一个数字列表,但我也为这个选择了协变限制”,现在可以理解为什么Java允许你写g1 = g2。g2保证是一个X<Y>,其中X是List的某个具体子类,而Y要么是Number,要么是其某个子类。

从类型上讲,这与“某种类型参数为协变的Number的列表”的概念是100%兼容的。你只能对List<? extends Number>执行在签名中的任何T为参数“禁用”的列表方法,而对于返回类型,则用边界(Number)来替换。

这正是List<? extends Number>在描述什么,因此是兼容的。

英文:

List&lt;? extends Number&gt; is best read as:

This is a list of numbers, but, covariantly.

In other words, this is a list of some concrete but unknown type. However, I do know that, whatever type it might be, at least it is either Number or some subclass thereof.

Generics is weird; once you opt into some variance, you get the restrictions to go along with that. In the case of collections, 'covariance' comes with the baggage of 'no adding'.

Try it.

g1.add(XXX);

the only thing that is legal for XXX here? null. That's literally it. The full and complete and exhaustive list of all you can add to this thing. certainly Number x = 5; g1.add(x); is not going to be allowed by javac here.

By writing List&lt;? extends a thingie&gt; you're saying: Yeah, I want that. I'm signing up to this restriction that I get to add absolutely nothing (other than the academic case of literal null). In trade for handcuffing yourself, the things you can pass in for g1 is expanded considerably.

You can also opt into contravariance:

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

contravariance is the opposite. add works. get doesn't work. Which in this case means: The type of the expression list.get(0) is just.. Object.


Now that we've covered that:

void test(List&lt;? extends Number&gt; g1, List&lt;? extends Number&gt; g2) {}

means 'my first parameter is a list of numbers, but I opt into covariance handcuffs', and 'my second parameter is a list of numbers, but I also opt into covariance handcuffs for this one too', it now makes sense why java lets you write g1 = g2. g2 is guaranteed to be an X&lt;Y&gt;, where X some concrete subclass of List, and Y is either Number or some subclass thereof.

This is 100% compatible, type-wise, with the notion of 'some sort of list whose type param is some covariant take on Number'. The only thing you can do a List&lt;? extends Number&gt; is to invoke methods of List where any T in the signatures are 'disabled' for parameters, and replaced by the bound (Number) for return types.

That's.. exactly what List&lt;? extends Number&gt; is describing, so it's compatible.

答案3

得分: 3

"我从各种关于Java泛型和通配符的描述中获得的印象是,每次使用通配符时,都会被捕获为一个全新的类型,"

这个陈述是正确的。

那又怎样?你正在混淆对象的类型与变量的类型。

考虑一下这段代码:

String s = &quot;abc&quot;;
Object o = s;

变量o的类型是Object,它可以赋值给变量s的类型。但这并不意味着String和Object是相同的类型。与你的示例没有区别。你有两种不同的List类型用于这些对象,但变量的类型是相同的。每个变量的类型都是List<? extends Number>,因此赋值是可以的。在进行赋值时,对象的泛型类型是List<x>,其中x是一个全新的未知类型。但变量的类型仍然是List<? extends Number>。

英文:

"I had the impression from various descriptions of Java generics and wildcards that each use of a wildcard is captured as a completely new type, "

That statement is correct.

So what? You are confusing the type of the object with the type of the variable.

Consider this code:

String s = &quot;abc&quot;;
Object o = s;

o has type Object which is assignment compatible with the type of s. But that doesn't mean String and Object are the same type. No different with your example. You have two different List types for the objects, but one type for the variables. Each variable has type List<? extends Number>, so the assignment is fine. When you make the assignment, the object's generic type is List&lt;x&gt; for some completely new unknown type x. But the variable type remains List<? extends Number>.

答案4

得分: 0

怎么可能是无效的?

两个变量具有相同的类型(在这种情况下是 List&lt;? extends Number&gt;),因此编译器必须允许将一个变量赋值给另一个变量。


分配给这些变量的对象可能具有不同的类型,但是变量类型相同,因此分配始终是合法的。

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

英文:

How could it not be valid?

Both variables have identical type (in this case List&lt;? extends Number&gt;), so the compiler must allow assignment of one to the other.


The objects assigned to the variables may have different types, but the variable types are identical, so assignment is always legal.

The compiler does not know or care what the actual type of an object assigned to a variable is, even if it can be determined from the code. It cares only about declared types when checking types.

huangapple
  • 本文由 发表于 2020年9月18日 07:27:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/63947339.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定