javagenericstypeserasure

Type Erasure in ArrayList<List<?>> generic call


Can someone explain why the following code does not compile:

ArrayList<List<?>> arrayList = new ArrayList<List<String>>();

Why is the above code invalid but this one is running fine:

ArrayList<?> items = new ArrayList<List<String>>();


Solution

  • Let's start with simple lists. Integer is a subclass of Number, so you can assign Integer to a Number:

    Integer i = 42;
    Number n = i;  // Ok
    

    Now, let's have a List<Number>. Shouldn't we be able to put a List<Integer> there?

    List<Number> numbers = new ArrayList<Integer>(); // Fails
    

    No, it fails. Why? Because generics in Java are invariant, that means for any two different types T1 and T2 neither List<T1> is a subtype of List<T2> nor List<T2> is a subtype of List<T1> regardless of relations between T1 and T2. Ok, this formally explains why the above assignment is invalid, but what is the logic behind this?

    The List<Number> should be able to hold any number, that means that if we have a declaration

    List<Number> numbers;
    

    then we should be able to do number.add(Integer(42)), number.add(Double(Math.PI)) and any other number.add(subClassOfNumber). However, if the above assignment

    List<Number> numbers = new ArrayList<Integer>();  // ?
    

    would be valid, then numbers would now hold the list, which is only capable of storing Integers, so numbers.add(anyNumberButInteger) would have failed, which violates the contract of List<Number>.

    It is possible to create a list, which will hold instances of some subclass of Number:

    List<? extends Number> list = Arrays.asList(5, 6, 7, 8);  // Ok
    

    You can read this list without a problem:

    System.out.println(list.get(2));   // 7
    

    However you can't put anything there except null, because it's not possible to know the exact type of list elements at compile time and thus is not possible to make the check for safety:

    List<? extends Number> list = ThreadLocalRandom.current().nextBoolean() ? 
                                       new ArrayList<Integer>() : 
                                       new ArrayList<Double>();
    list.add(null);      // Ok
    list.add(Double.valueOf(3.1415));    //  Fails, because we don't know if Double(3.1415) is actually compatible with elements of the list
    

    Well, now to your example:

    List<List<?>> listOfLists = new ArrayList<List<String>>();
    

    In order this assignment to work you actually need to have on the left side a List<? extends List<String>>, but since Java generic is invariant, the only type which extends List<String> is List<String> itself, so the only valid combination is

    List<List<String>> listOfListsOfStrings = new ArrayList<List<String>>();
    

    It is possible to create a variable which represent a list, which is composed of some lists by using raw types:

    List<? extends List> listOfLists = new ArrayList<List<String>>(); // Compiles, but loses generics. DON'T DO THIS!
    List<String> listOfStrings = listOfLists.get(0);                  // Compiles, even would work as expected assuming we populated listOfLists correctly
    List<Integer> listOfIntegers = listOfLists.get(0);                // Compiles, but does not work as expected (fail at runtime when accessing elements)
    

    TLDR: the only generic class which is super/extends List<String> is List<String>, so List<some class which extends List<String>> automatically means List<List<String>>.