Difference of assignability with nested wildcards

2020-02-10 06:12发布

The following compiles just fine in JDK8, but gives an incompatible types error with JDK7.

List<List<? extends Number>> xs = Arrays.asList(Arrays.asList(0));

According to this answer, List<List<? extends Number>> doesn't have a supertype relationship to List<List<Integer>>.

What changed in Java 8 that made this assignment work? I'm also having a hard time understanding why it doesn't work in Java 7.


Both of these statements compile without type error using JDK7:

List<? extends Number> xs = Arrays.asList(0);
List<? extends List<? extends Number>> ys = Arrays.asList(Arrays.asList(0));

It seems very unintuitive to me that both of those work in JDK7, but the original example above does not. All of them of course will work in JDK8. I think to really understand what's going on here I'd need to understand why these examples are legal in Java 7 but the original example is not.

2条回答
三岁会撩人
2楼-- · 2020-02-10 06:26

It is quite simple:

In Java 7, the context of a method call was not considered when inferring type arguments. The only thing that was considered where the arguments of a method call.

In your case, int is boxed to Integer which yields List<Integer> as type for the inner call and then List<List<Integer>> as type for the outer call. Now you have a problem, since you want to assign the result to a variable of type List<List<? extends Number>> and this is just not possible since generics are invariant as long as there are no wildcards used, i.e., List<X> can never be converted to List<Y>. In your case X is List<Integer> and Y is List<? extends Number>. Even if List<? extends Number> contains a wildcard, it does not use a wildcard itself, i.e., it is not ? extends List<? extends Number>. This is why it does not compile in Java 7.

I know understanding generics and variance and wildcards is not that easy. Maybe I can clarify it like this for you:

  1. Usually, A<Y> is never considered a subtype of any A<Z>.
  2. However, if Z inherits from Y, then A<Z> is a subtype of A<? extends Y>.
  3. Now to the nested generics: We do know, that the inner types (A<Z> and A<? extends Y> are related in a subtype relation. But if we wrap them in another generic type, like B<A<Z>> and B<A<? extends Y>> then rule 1. applies: Since there are no wildcards, the second B is not considered a subtype of the first one. If we again introduce a wildcard, then they are: B<A<Z>> is indeed a subtype of B<? extends A<? extends Y>>. But now note that the outher ? extends is missing in your example, so it does not compile in Java 7.

Now to Java 8. Java 8 also takes the context of a call into account when inferring type arguments. Thus, Java 8 considers that you want to pass the result of the Arrays.asList calls to a variable of type List<List<? extends Number>>. It therefore tries to find type arguments that will make this assignment legal. It then infers that the type argument for the inner call must be Number, cause otherwise the assignment would not be legal.

To cut it short: Java 8 is just a lot more clever than Java 7 when choosing type arguments, since it also looks at the context, not only the arguments.

查看更多
迷人小祖宗
3楼-- · 2020-02-10 06:27

I believe this has to do with invocation contexts and widening reference conversion.

Basically, in that invocation context, the type of the argument 0 in Arrays.asList(0) can be boxed to Integer and then widened to Number. When that happens, Arrays.asList(0) has a return type of List<Number>. Through the same process, that List<Number> can be converted to List<? extends Number> before being used as an argument to the outer Arrays.asList(..).

This is equivalent to using explicit type arguments in Java 7

List<List<? extends Number>> xs = Arrays.<List<? extends Number>>asList(Arrays.<Number>asList(0));

In Java 7, the type of an expression is the same regardless of where it is used, whether it's a standalone expression or used in assignment expression.

Java 8 introduced poly-expressions in which the type of the expression could be influenced by the target type of the expression.

For example, in the following assignment expression

List<Number> numbers = Arrays.asList(1);

The type of the expression Arrays.asList(1) is the return type of the method being invoked which depends entirely on the generic type parameter. In this case, that type argument will be inferred as Integer because the value 1 can be converted to Integer through boxing conversion (primitives can't be used with generics). So the type of the expression is List<Integer>.

In Java 7, this assignment expression would not compile because List<Integer> cannot be assigned to List<Number>. This could be fixed by providing an explicit type argument when invoking asList

List<Number> numbers = Arrays.<Number>asList(1);

in which case, the method invocation expects a Number argument for its first parameter and the value 1 satisfies that.

In Java 8, the assignment expression is a poly expression

A method invocation expression is a poly expression if all of the following are true:

  • The invocation appears in an assignment context or an invocation context (§5.2, §5.3).

  • If the invocation is qualified (that is, any form of MethodInvocation except for the first), then the invocation elides TypeArguments to the left of the Identifier.

  • The method to be invoked, as determined by the following subsections, is generic (§8.4.4) and has a return type that mentions at least one of the method's type parameters.

Being a poly expression, it can be influenced by the type of the variable it is being assigned to. And that's what happens. The generic type Number influences the type argument inferred in the invocation of Arrays.asList(1).

Note how it wouldn't work in the following example

List<Number> numbers = ...;
List<Integer> integers = ...; // integers is not a poly expression
numbers = integers; // nope

So it's not covariance, but we get some of its benefits in some places.

查看更多
登录 后发表回答