Why is this generic assignment illegal?
Asked Answered
S

1

10

I have a class:

class Generic<T> {
    List<List<T>> getList() {
        return null;
    }
}

When I declare a Generic with wildcard and call getList method, the following assignment is illegal.

Generic<? extends Number> tt = null;
List<List<? extends Number>> list = tt.getList(); // this line gives compile error

This seems odd to me because according to the declaration of Generic, it's natural to create a Generic<T> and get a List<List<T>> when call getList.

In fact, it require me to write assignment like this:

List<? extends List<? extends Number>> list = tt.getList(); // this one is correct

I want to know why the first one is illegal and why the second one is legal.

The example I give is just some sample code to illustrate the problem, you don't have to care about their meaning.

The error message:

Incompatable types:
required : List<java.util.List<? extends java.lang.Number>>
found: List<java.util.List<capture<? extends java.lang.Number>>>

Serialize answered 28/9, 2020 at 6:32 Comment(6)
When asking about errors you might want to post them, i.e. the exact message and ideally the stack trace.Oireachtas
It's not runtime excpetion, it's just compile error.Serialize
What does the compile error say?Saeger
Read about wildcards, then try to avoid them.Aenneea
In fact I know what the error message means,I just want to know why the compiler think this assigment is illegal. In other word, if it is legal, what goes wrong.Serialize
Are you sure thats is the error message? java.utils.List is non-standard, in the JDK that would be java.util.List.Inwards
H
14

This is a tricky but interesting thing about wildcard types that you have run into! It is tricky but really logical when you understand it.

The error has to do with the fact that the wildcard ? extends Number does not refer to one single concrete type, but to some unknown type. Thus two occurrences of ? extend Number don't necessarily refer to the same type, so the compiler can't allow the assignment.

Detailed explanation

  1. The right-hand-side in the assignment, tt.getList(), does not get the type List<List<? extends Number>>. Instead each use of it is assigned by the compiler a unique generated capture type, for exampled called List<List<capture#1 extends Number>>.

  2. The capture type List<capture#1 extends Number> is a subtype of List<? extends Number>, but it is not type same type! (This is to avoid mixing different unknown types together.)

  3. The type of the left-hand-side in the assignment is List<List<? extends Number>>. This type does not allow subtypes of List<? extends Number> to be the element type of the outer list, thus the return type of getList can't be used as the element type.

  4. The type List<? extends List<? extends Number>> on the other hand does allow subtypes of List<? extends Number> as the element type of the outer list. So that is the right fix for the problem.

Motivation

The following example code demonstrates why the assignment is illegal. Through a sequence of steps we end up with a List<Integer> which actually contains Floats!

class Generic<T> {
    private List<List<T>> list = new ArrayList<>();

    public List<List<T>> getList() {
        return list;
    }
}

// Start with a concrete type, which will get corrupted later on
Generic<Integer> genInt = new Generic<>();

// Add a List<Integer> to genInt.list. This is not necessary for the
// main example but migh make things a little clearer.
List<Integer> ints = List.of(1);
genInt.getList().add(ints); 

// Assign to a wildcard type as in the question
Generic<? extends Number> genWild = genInt;

// The illegal assignment. This doesn't compile normally, but we force it
// using an unchecked cast to see what would happen IF it did compile.
List<List<? extends Number>> list =
    (List<List<? extends Number>>) (Object) genWild.getList();

// This is the crucial step: 
// It is legal to add a List<Float> to List<List<? extends Number>>.
// list refers to genInt.list, which has type List<List<Integer>>.
// Heap pollution occurs!
List<Float> floats = List.of(1.0f);
list.add(floats);

// notInts in reality is the same list as floats!
List<Integer> notInts = genInt.getList().get(1);

// This statement reads a Float from a List<Integer>. A ClassCastException
// is thrown. The compiler must not allow us to end up here without any
// previous type errors or unchecked cast warnings.
Integer i = notInts.get(0);

The fix that you discovered was to use the following type for list:

List<? extends List<? extends Number>> list = tt.getList();

This new type shifts the type error from the assignment of list to the call to list.add(...).

The above illustrates the whole point of wildcard types: To keep track of where it is safe to read and write values without mixing up types and getting unexpected ClassCastExceptions.

General rule of thumb

There is a general rule of thumb for situations like this, when you have nested type arguments with wildcards:

If the inner types have wildcards in them, then the outer types often need wildcards also.

Otherwise the inner wildcard can't "take effect", in the way you have seen.

References

The Java Tutorial contains some information about capture types.

This question has answers with general information about wildcards:

What is PECS (Producer Extends Consumer Super)?

Hyoscyamus answered 28/9, 2020 at 7:11 Comment(7)
Just for my interest in it: Can you declare var list= tt.getList(); to get a List<List<capture#1 extends Number>>, or doesn't it work with generics?Shoveler
Do you mean List<capture#1 extends Number> is a subype of List<? extends Number> , not List<List<capture#1 extends Number>> is a subtype of List<List<? extends Number>> ?Serialize
@haoyuwang: Yes, exactly! Sorry, I made a mistake there, will fix.Hyoscyamus
@DorianGray: I believe it is like you say, that you can use var to get a variable with exactly the capture type. But I don't think you can do much with that type anyway...Hyoscyamus
@Hyoscyamus Could you give some example about what will happen if the first assignment is legal. It doesn't have to be about my case as long as it helps to understand the problem. (my case is bad). I would appreciate that.Serialize
@Hyoscyamus Most likely not, I just find that var declaration especially useful in such situations where the return type is hard to guess.Shoveler
@haoyuwang: I added an example to the answer. This is really pretty tricky...Hyoscyamus

© 2022 - 2024 — McMap. All rights reserved.