Difference of assignability with nested wildcards in Java 7/8 generics
Asked Answered
G

2

9

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.

Geneviegenevieve answered 12/6, 2014 at 15:56 Comment(5)
Seems to me that because Arrays.asList(0) would return a List<Integer>, and Arrays.asList() of that would return a List whose elements are List<Integer>, seems to me that the assignment List<List<? extends Number>> would actually be right... I'm almost certainly misunderstanding something thoughPropriety
@SotiriosDelimanolis Aaaaand turns out I forgot to change the line back from when I was experimenting with it. I get failures too.Propriety
@user3580294 - "... seems to me that the assignment List<List<? extends Number>> would actually be right..." That's exactly what I'm confused about. The Java 8 behavior seems like the intuitively correct behavior to me, so I don't understand why there's a type error in Java 7. What's the rationale behind that? Apparently they decided to fix it in Java 8, but I can't figure out what exactly they changed to make this work.Geneviegenevieve
@Geneviegenevieve I've heard that there were improvements in the JLS as to how type inference/detection/checking works in Java 8, so it could be possible that the spec didn't allow for it back then. I can't say for sure though. Quite an intriguing question....Propriety
The correct idiom for Java 7 would be List<List<? extends Number>> ys = Arrays.<List<? extends Number>asList(Arrays.asList(0));. It shows that even under Java 7 the assignment is correct and only an issue of the limited type-inference.Smokeless
T
10

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.

Taxation answered 12/6, 2014 at 17:3 Comment(6)
How in the world do you find these things buried in the JLS?Propriety
@user3580294 I recently read part of an article by Angelika Langer that spoke about invocation contexts (although I think it was before they were applied in Java 8). I remembered the term and just ctrl+f'ed.Taxation
I don't see any significant differences in the sections you've linked between Java 7 and 8: (Java 7) Method Invocation Conversion and (Java 7) Widening Reference Conversion. Also note that I've updated my question with a couple more examples.Geneviegenevieve
@Dao Then I should also add this. Basically, that invocation is a poly expression, where a poly expression can be influenced by the target type of the expression.Taxation
@Geneviegenevieve I'm at work right now, but I'll get back to it later if you don't have the answers you seek.Taxation
@Geneviegenevieve I've added some details. Let me know what you think.Taxation
P
3

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.

Palmar answered 12/6, 2014 at 17:11 Comment(6)
Isn't a List<Integer> a List<? extends Number> though? I thought wildcards allow for covariance such as this.Propriety
List<? extends Number> xs = Arrays.asList(0); compiles without error with JDK7. Doesn't that contradict your answer? (Note that I've updated my question with this example.)Geneviegenevieve
@user3580294: They do! But wildcards are not transitive. Just because List<Integer> is a List<? extends Number> does not mean that List<List<Integer>>` is a List<List<? extends Number>>.Palmar
@DaoWen: Look at my comment above. I have clarified my answer. The problem are the nested generics.Palmar
Ah, that's true. Looks like I got the lists backwards. Thanks!Propriety
I feel like Sotirios answered my core question, but you cleared up my confusion about the wildcards. To try to be fair, I'm upvoting your answer, but accepting (and not upvoting) the other answer.Geneviegenevieve

© 2022 - 2024 — McMap. All rights reserved.