这两种通用方法有什么区别?
What's difference between these two generic methods?
抱歉,标题太抽象了,但我不知道如何将这个问题压缩成一个句子。
我不明白为什么 call1
编译失败,而 call2
运行良好。 call1
和 call2
在逻辑上不是一样的吗?
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
}
}
这可能确实是一个令人惊讶的行为,因为这两种方法都产生完全相同的字节码(方法的签名除外)。为什么第一次调用会产生 compile-time 错误
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
首先,当你调用一个方法时,你必须能够将实际参数分配给方法的实参。所以,如果你有一个方法
void method(SomeClass arg) {}
你称它为
method(someArgument);
则以下赋值必须有效:
SomeClass arg = someArgument;
仅当 someArgument
的类型是 SomeClass
.
的子class 类型时,此赋值才有可能
现在,Java 中的数组是协变的,如果某些 subClass
是某些 superClass
的子class,则 subClass[]
是子class 共 superClass[]
。也就是说下面的方法
void method(superClass[] arg) {}
可以用 class subClass[]
.
参数调用
但是,Java 中的泛型是不变的。对于任何两种不同的类型 T
和 U
,classes List<T>
和 List<U>
既不是 subclasses 也不是 superclass es 彼此,即使原始类型 T
和 U
之间存在这种关系。如您所知,您可以使用通配符将 List
参数传递给方法:
void method(List<?> arg) {}
但这意味着什么?正如我们所见,为了使调用工作,以下分配必须有效:
List<?> arg = ListOfAnyType;
这意味着,List<?>
成为所有可能列表的超级class。这是很重要的一点。 List<?>
的类型不是任何列表的任何类型,因为其中none可以相互赋值,是一个特殊的类型,可从任何列表分配。比较以下两种类型: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
的类型是一个列表,其中包含元素,其类型是某种(未知)类型的列表。因此,我们可以将任何列表列表传递给 method2。但是,第一种方法的参数 arg1
具有更复杂的类型。它是一个元素列表,它有一个非常特殊的类型,它是任何列表的 superclass。同样,虽然任何列表 的超级 class 都可以从任何列表分配 ,但 它不是 列表本身。而且,因为 Java 中的泛型是不变的,这意味着由于 List<?>
不同于任何类型的列表,因此 List<List<?>>
的类型不同于任何 List<List<of_any_type>>
。这就是编译错误的意思
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
类型list of lists of some type不同于list of superclass of all lists,因为某种类型的列表不同于所有列表superclass.
现在,如果您理解了所有这些内容,您就可以找到一种方法来调用第一个方法。你需要的是另一个通配符:
private void call1(final List<? extends List<? extends Parent>> testList)
现在调用此方法将编译。
更新
也许类型依赖关系的图形表示将有助于更容易地理解它。考虑两个 classes:Parent 和扩展 Parent 的 Child。
Parent <--- Child
您可以有一个 Parent 列表和一个 Child 列表,但是由于泛型不变性,这些 classes 彼此不相关:
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 时,我们打破了所有 superclass-subclass 依赖(记住泛型的不变性):
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>>
因此,我们可以将列表列表中的任何一个传递给 method3。但是,以下声明:
void method4(List<List<? extends Parent>> arg4)
只允许最后一个类型作为 method4 的参数:
List<List<Parent>>
List<List<Child>>
List<List<? extends Parent>> - possible type for arg4
因此,我们无法将列表列表中的任何一个传递给 method4。
我们可以使用另一个通配符为所有三种类型创建一个新的 superclass:
List<List<Parent>>
/
/
List<? extends List<? extends Parent>> --- List<List<Child>>
\
\
List<List<? extends Parent>>
现在如果我们使用这个类型作为我们方法的参数
void method5(List<? extends List<? extends Parent>> arg5)
我们将能够将任一列表列表传递给 method5。
总结一下: 看起来很相似,声明 <T extends SomeClass> List<T>
和List<? extends SomeClass>
的工作方式大不相同。第一个声明表示变量可以具有 List<SubClass1 of SomeClass>
、List<SubClass2 of SomeClass>
或 List<SubClass3 of SomeClass>
等可能的类型之一。第二个声明创建了一个特殊类型,它成为一个 super[= List<SubClass1 of SomeClass>
、List<SubClass2 of SomeClass>
、List<SubClass3 of SomeClass>
等的所有 169=]
抱歉,标题太抽象了,但我不知道如何将这个问题压缩成一个句子。
我不明白为什么 call1
编译失败,而 call2
运行良好。 call1
和 call2
在逻辑上不是一样的吗?
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
}
}
这可能确实是一个令人惊讶的行为,因为这两种方法都产生完全相同的字节码(方法的签名除外)。为什么第一次调用会产生 compile-time 错误
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
的子class,则 subClass[]
是子class 共 superClass[]
。也就是说下面的方法
void method(superClass[] arg) {}
可以用 class subClass[]
.
但是,Java 中的泛型是不变的。对于任何两种不同的类型 T
和 U
,classes List<T>
和 List<U>
既不是 subclasses 也不是 superclass es 彼此,即使原始类型 T
和 U
之间存在这种关系。如您所知,您可以使用通配符将 List
参数传递给方法:
void method(List<?> arg) {}
但这意味着什么?正如我们所见,为了使调用工作,以下分配必须有效:
List<?> arg = ListOfAnyType;
这意味着,List<?>
成为所有可能列表的超级class。这是很重要的一点。 List<?>
的类型不是任何列表的任何类型,因为其中none可以相互赋值,是一个特殊的类型,可从任何列表分配。比较以下两种类型: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
的类型是一个列表,其中包含元素,其类型是某种(未知)类型的列表。因此,我们可以将任何列表列表传递给 method2。但是,第一种方法的参数 arg1
具有更复杂的类型。它是一个元素列表,它有一个非常特殊的类型,它是任何列表的 superclass。同样,虽然任何列表 的超级 class 都可以从任何列表分配 ,但 它不是 列表本身。而且,因为 Java 中的泛型是不变的,这意味着由于 List<?>
不同于任何类型的列表,因此 List<List<?>>
的类型不同于任何 List<List<of_any_type>>
。这就是编译错误的意思
incompatible types: List<List<Parent>> cannot be converted to List<List<? extends Parent>>
类型list of lists of some type不同于list of superclass of all lists,因为某种类型的列表不同于所有列表superclass.
现在,如果您理解了所有这些内容,您就可以找到一种方法来调用第一个方法。你需要的是另一个通配符:
private void call1(final List<? extends List<? extends Parent>> testList)
现在调用此方法将编译。
更新
也许类型依赖关系的图形表示将有助于更容易地理解它。考虑两个 classes:Parent 和扩展 Parent 的 Child。
Parent <--- Child
您可以有一个 Parent 列表和一个 Child 列表,但是由于泛型不变性,这些 classes 彼此不相关:
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 时,我们打破了所有 superclass-subclass 依赖(记住泛型的不变性):
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>>
因此,我们可以将列表列表中的任何一个传递给 method3。但是,以下声明:
void method4(List<List<? extends Parent>> arg4)
只允许最后一个类型作为 method4 的参数:
List<List<Parent>>
List<List<Child>>
List<List<? extends Parent>> - possible type for arg4
因此,我们无法将列表列表中的任何一个传递给 method4。 我们可以使用另一个通配符为所有三种类型创建一个新的 superclass:
List<List<Parent>>
/
/
List<? extends List<? extends Parent>> --- List<List<Child>>
\
\
List<List<? extends Parent>>
现在如果我们使用这个类型作为我们方法的参数
void method5(List<? extends List<? extends Parent>> arg5)
我们将能够将任一列表列表传递给 method5。
总结一下: 看起来很相似,声明 <T extends SomeClass> List<T>
和List<? extends SomeClass>
的工作方式大不相同。第一个声明表示变量可以具有 List<SubClass1 of SomeClass>
、List<SubClass2 of SomeClass>
或 List<SubClass3 of SomeClass>
等可能的类型之一。第二个声明创建了一个特殊类型,它成为一个 super[= List<SubClass1 of SomeClass>
、List<SubClass2 of SomeClass>
、List<SubClass3 of SomeClass>
等的所有 169=]