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.
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.
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:
- Usually,
A<Y>
is never considered a subtype of any A<Z>
.
- However, if
Z
inherits from Y
, then A<Z>
is a subtype of A<? extends Y>
.
- 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.