Sorry for too abstract title, but I have no idea how to compress this question into a single sentence.
I cannot figure out why call1
is failed to be compiled while call2
works well. Aren't call1
and call2
logically same?
public class Test {
@AllArgsConstructor
@Getter
private static class Parent {
private String name;
}
private static class Child extends Parent {
Child(final String name) {
super(name);
}
}
private void call1(final List<List<? extends Parent>> testList) {
System.out.println(testList.get(0).get(0).getName());
}
private <T extends Parent> void call2(final List<List<T>> testList) {
System.out.println(testList.get(0).get(0).getName());
}
public void show() {
final List<List<Parent>> nestedList1 = List.of(List.of(new Parent("P1")));
final List<List<Child>> nestedList2 = List.of(List.of(new Child("C1")));
call1(nestedList1); // compile error
call1(nestedList2); // compile error
call2(nestedList1); // okay
call2(nestedList2); // okay
}
}
CodePudding user response:
This might be indeed a surprising behavior, because both methods produce exactly the same bytecode (except the signature of the methods). Why then the first call produces a compile-time error
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
First of all, when you call a method you must be able to assign the actual parameters to the arguments of the method. So, if you have a method
void method(SomeClass arg) {}
and you call it as
method(someArgument);
then the following assignment must be valid:
SomeClass arg = someArgument;
This assignment is possible only if the type of someArgument
is a subclass of SomeClass
.
Now, arrays in Java are covariant, if some subClass
is a subclass of some superClass
, then subClass[]
is a subclass of superClass[]
. That means that the following method
void method(superClass[] arg) {}
can be called with argument of class subClass[]
.
However, generics in Java are invariant. For any two different types T
and U
the classes List<T>
and List<U>
are neither subclasses nor superclasses of each other, even if such relation exists between the original types T
and U
. As you know you can pass a List
argument to a method by using a wildcard:
void method(List<?> arg) {}
But what does this mean? As we have seen, in order for the call to work the following assignment must be valid:
List<?> arg = ListOfAnyType;
That means, that List<?>
becomes a superclass for all possible lists. This is an important point. The type of List<?>
is not any type of any list, because none of them can be assigned to each other, it's a special type, which is assignable from any list. Compare the following two types: List<?>
and List<Object>
. The first one can contain any type of list, while the second one can contain list of any type of objects. So, if you have two methods:
void method1(List<?> arg) {}
void <T> method2(List<T> arg) {}
then the first one will accept any list as an argument, while the second one will accept as an argument the list of any type of objects. Well, both methods work in the same way, but the difference becomes important when we add a new level of generics:
void method1(List<List<?>> arg1) {}
void <T> method2(List<List<T>> arg2) {}
The second case is simple. The type of arg2
is a list, which contains elements, whose type is a list of some (unknown) type. Because of that we can pass any list of lists to method2. The argument arg1
of the first method, however, has a more complex type. It's a list of elements, which have a very special type which is a superclass of any list. Again, while superclass of any list can be assigned from any list, it is not a list itself. And, because the generics in Java is invariant, it means that since List<?>
differs from any type of a list, the type of List<List<?>>
differs from any List<List<of_any_type>>
. That's the meaning of the compile error
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
The type list of lists of some type is different from list of superclass of all lists, because list of some type is different from superclass of all lists.
Now, if you understood all of this you can find a way to make the call to the first method work. What you need is another wildcard:
private void call1(final List<? extends List<? extends Parent>> testList)
Now the calls to this method will compile.
UPDATE
Maybe a graphical representation of types' dependences will help to understand it easier. Consider two classes: Parent, and Child which extends Parent.
Parent <--- Child
You can have a list of Parent, and a list of Child, but because of generics invariance these classes are not related to each other:
List<Parent>
List<Child>
When you create a parameterized method
<T extends Parent> void method1(List<T> arg1)
you tell Java that the argument arg1
will be either List<Parent>
or List<Child>
:
List<Parent> - possible type for arg1
List<Child> - possible type for arg1
However, when you create a method with a wildcard argument
void method2(List<? extends Parent> arg2)
you tell Java to create a new special type, which is a supertype for both List<Parent>
and List<Child>
, and make arg2
a variable of this type:
List<Parent>
/
/
List<? extends Parent>
^ \
| \
| List<Child>
|
the exact type of arg2
Because the type of arg2
is a supertype of both List<Parent>
and List<Child>
, you can pass either of these lists to method2.
Now let's introduce the next level of generics. When we wrap the above diagram into a new List, we break all the superclass-subclass dependencies (remember the invariance of generics):
List<List<Parent>> all of these three classes
List<List<Child>> are independent
List<List<? extends Parent>> of each other
When we create the following method:
<T extends Parent> void method3(List<List<T>> arg3)
we tell Java, that arg3
can be either List<List<Parent>>
or List<List<Child>>
:
List<List<Parent>> - possible type for arg3
List<List<Child>> - possible type for arg3
List<List<? extends Parent>>
Thus, we can pass either of the list of lists to method3. However, the following declaration:
void method4(List<List<? extends Parent>> arg4)
allows only the last type to be the argument for method4:
List<List<Parent>>
List<List<Child>>
List<List<? extends Parent>> - possible type for arg4
Consequently, we cannot pass either of the list of lists to method4. We can use another wildcard to create a new superclass for all three types:
List<List<Parent>>
/
/
List<? extends List<? extends Parent>> --- List<List<Child>>
\
\
List<List<? extends Parent>>
Now if we use this type as an argument to our method
void method5(List<? extends List<? extends Parent>> arg5)
we'll be able to pass either of the lists of lists to method5.
To summarize: while looking similarly, declarations <T extends SomeClass> List<T>
and List<? extends SomeClass>
are working very differently. The first declaration says that the variable can have one of possible types List<SubClass1 of SomeClass>
, or List<SubClass2 of SomeClass>
, or List<SubClass3 of SomeClass>
, etc. The second declaration creates a single special type, which becomes a superclass of all of List<SubClass1 of SomeClass>
, and List<SubClass2 of SomeClass>
, and List<SubClass3 of SomeClass>
, etc.