这可能确实是一个令人惊讶的行为,因为这两种方法产生完全相同的字节码(除了方法的签名)。那么为什么第一次调用会产生编译时错误
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
首先,当您调用一个方法时,您必须能够将实际参数分配给该方法的参数。所以,如果你有一个方法
void method(SomeClass arg) {}
你称之为
method(someArgument);
那么下面的赋值一定是有效的:
SomeClass arg = someArgument;
仅当someArgument 的类型是SomeClass 的子类时,此分配才可能。
现在,Java 中的数组是协变的,如果一些 subClass 是一些 superClass 的子类,那么 subClass[] 是 superClass[] 的子类。那就是说下面的方法
void method(superClass[] arg) {}
可以使用subClass[]类的参数调用。
然而,Java 中的泛型是不变的。对于任何两个不同的类型T 和U,类List<T> 和List<U> 既不是彼此的子类也不是彼此的超类,即使原始类型T 和U 之间存在这种关系。如您所知,您可以使用通配符将 List 参数传递给方法:
void method(List<?> arg) {}
但这意味着什么?正如我们所见,为了使调用生效,以下分配必须有效:
List<?> arg = ListOfAnyType;
这意味着,List<?> 成为所有可能列表的超类。这是很重要的一点。 List<?>的类型不是任何列表的任何类型,因为它们都不能相互赋值,它是一种特殊的类型,可以从任何列表中赋值.比较以下两种类型:List<?> 和 List<Object>。第一个可以包含任何类型的列表,而第二个可以包含任何类型的对象的列表。所以,如果你有两种方法:
void method1(List<?> arg) {}
void <T> method2(List<T> arg) {}
那么第一个将接受任何列表作为参数,而第二个将接受任何类型对象的列表作为参数。好吧,这两种方法的工作方式相同,但是当我们添加新级别的泛型时,区别就变得很重要了:
void method1(List<List<?>> arg1) {}
void <T> method2(List<List<T>> arg2) {}
第二种情况很简单。 arg2 的类型是一个列表,其中包含元素,其类型是某个(未知)类型的列表。因此,我们可以将任何列表列表传递给方法 2。然而,第一种方法的参数arg1 具有更复杂的类型。它是一个元素列表,具有非常特殊的类型,是任何列表的超类。同样,虽然任何列表的超类可以从任何列表中分配,但它不是列表本身。而且,因为 Java 中的泛型是不变的,这意味着由于 List<?> 不同于任何类型的列表,List<List<?>> 的类型不同于任何 List<List<of_any_type>>。这就是编译错误的意思
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
类型某种类型的列表不同于所有列表的超类列表,因为某种类型的列表不同于所有列表的超类。
现在,如果您了解所有这些,您可以找到一种方法来调用第一个方法。你需要的是另一个通配符:
private void call1(final List<? extends List<? extends Parent>> testList)
现在对该方法的调用将编译。
更新
也许类型依赖性的图形表示将有助于更容易理解它。考虑两个类:Parent 和继承 Parent 的 Child。
Parent <--- Child
您可以有一个 Parent 列表和一个 Child 列表,但由于泛型不变性,这些类彼此不相关:
List<Parent>
List<Child>
当你创建参数化方法时
<T extends Parent> void method1(List<T> arg1)
您告诉 Java 参数 arg1 将是 List<Parent> 或 List<Child>:
List<Parent> - possible type for arg1
List<Child> - possible type for arg1
但是,当您使用通配符参数创建方法时
void method2(List<? extends Parent> arg2)
您告诉 Java 创建一个新的特殊类型,它是 List<Parent> 和 List<Child> 的超类型,并让 arg2 成为这种类型的变量:
List<Parent>
/
/
List<? extends Parent>
^ \
| \
| List<Child>
|
the exact type of arg2
因为arg2 的类型是List<Parent> 和List<Child> 的超类型,所以您可以将这两个列表中的任何一个传递给method2。
现在让我们介绍下一个级别的泛型。当我们将上图包装成一个新的 List 时,我们打破了所有超类-子类的依赖关系(记住泛型的不变性):
List<List<Parent>> all of these three classes
List<List<Child>> are independent
List<List<? extends Parent>> of each other
当我们创建以下方法时:
<T extends Parent> void method3(List<List<T>> arg3)
我们告诉 Java,arg3 可以是 List<List<Parent>> 或 List<List<Child>>:
List<List<Parent>> - possible type for arg3
List<List<Child>> - possible type for arg3
List<List<? extends Parent>>
因此,我们可以将列表列表中的任何一个传递给方法 3。但是,以下声明:
void method4(List<List<? extends Parent>> arg4)
只允许最后一个类型作为方法 4 的参数:
List<List<Parent>>
List<List<Child>>
List<List<? extends Parent>> - possible type for arg4
因此,我们不能将列表列表中的任何一个传递给方法 4。
我们可以使用另一个通配符为所有三种类型创建一个新的超类:
List<List<Parent>>
/
/
List<? extends List<? extends Parent>> --- List<List<Child>>
\
\
List<List<? extends Parent>>
现在如果我们使用这个类型作为我们方法的参数
void method5(List<? extends List<? extends Parent>> arg5)
我们将能够将任一列表列表传递给方法 5。
总结:虽然看起来相似,但声明 <T extends SomeClass> List<T> 和 List<? extends SomeClass> 的工作方式却大不相同。第一个声明说变量可以具有List<SubClass1 of SomeClass>、List<SubClass2 of SomeClass>、或List<SubClass3 of SomeClass>等可能的类型之一。第二个声明创建一个特殊类型,它成为所有List<SubClass1 of SomeClass>的超类,并且List<SubClass2 of SomeClass>、List<SubClass3 of SomeClass>等