Why is this generic assignment illegal?

442 views Asked by At

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>>>

1

There are 1 answers

7
Lii On BEST ANSWER

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)?