这两种通用方法有什么区别?

What's difference between these two generic methods?

抱歉,标题太抽象了,但我不知道如何将这个问题压缩成一个句子。

我不明白为什么 call1 编译失败,而 call2 运行良好。 call1call2 在逻辑上不是一样的吗?

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 中的泛型是不变的。对于任何两种不同的类型 TU,classes List<T>List<U> 既不是 subclasses 也不是 superclass es 彼此,即使原始类型 TU 之间存在这种关系。如您所知,您可以使用通配符将 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=]