【问题标题】:What's difference between these two generic methods?这两种通用方法有什么区别?
【发布时间】:2022-02-11 18:46:43
【问题描述】:

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

我不明白为什么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
    }
}

【问题讨论】:

  • 真正的问题是为什么 call1() 不起作用而以下起作用:private void callSingleList(final List&lt;? extends Parent&gt; testList) { System.out.println(testList.get(0).getName()); }
  • 这种差异仅在所需类型为 NESTED 泛型类型时才会出现。 (List&lt;? extends Parent&gt;&lt;T extends Prent&gt; List&lt;T&gt; 工作方式相同,但 List&lt;List&lt;? extends Parent&gt;&gt;&lt;T extends Prent&gt; List&lt;List&lt;T&gt;&gt; 工作方式不同。)

标签: java generics


【解决方案1】:

这可能确实是一个令人惊讶的行为,因为这两种方法产生完全相同的字节码(除了方法的签名)。那么为什么第一次调用会产生编译时错误

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 中的泛型是不变的。对于任何两个不同的类型TU,类List&lt;T&gt;List&lt;U&gt; 既不是彼此的子类也不是彼此的超类,即使原始类型TU 之间存在这种关系。如您所知,您可以使用通配符将 List 参数传递给方法:

void method(List<?> arg) {}

但这意味着什么?正如我们所见,为了使调用生效,以下分配必须有效:

List<?> arg = ListOfAnyType;

这意味着,List&lt;?&gt; 成为所有可能列表的超类。这是很重要的一点。 List&lt;?&gt;的类型不是任何列表的任何类型,因为它们都不能相互赋值,它是一种特殊的类型,可以从任何列表中赋值.比较以下两种类型:List&lt;?&gt;List&lt;Object&gt;。第一个可以包含任何类型的列表,而第二个可以包含任何类型的对象的列表。所以,如果你有两种方法:

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&lt;?&gt; 不同于任何类型的列表,List&lt;List&lt;?&gt;&gt; 的类型不同于任何 List&lt;List&lt;of_any_type&gt;&gt;。这就是编译错误的意思

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&lt;Parent&gt;List&lt;Child&gt;

List<Parent> - possible type for arg1
List<Child>  - possible type for arg1

但是,当您使用通配符参数创建方法时

void method2(List<? extends Parent> arg2) 

您告诉 Java 创建一个新的特殊类型,它是 List&lt;Parent&gt;List&lt;Child&gt; 的超类型,并让 arg2 成为这种类型的变量:

                        List<Parent>
                       /
                      /
List<? extends Parent>
          ^           \
          |            \
          |             List<Child>
          |
 the exact type of arg2

因为arg2 的类型是List&lt;Parent&gt;List&lt;Child&gt; 的超类型,所以您可以将这两个列表中的任何一个传递给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&lt;List&lt;Parent&gt;&gt;List&lt;List&lt;Child&gt;&gt;

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。

总结:虽然看起来相似,但声明 &lt;T extends SomeClass&gt; List&lt;T&gt;List&lt;? extends SomeClass&gt; 的工作方式却大不相同。第一个声明说变量可以具有List&lt;SubClass1 of SomeClass&gt;List&lt;SubClass2 of SomeClass&gt;、或List&lt;SubClass3 of SomeClass&gt;等可能的类型之一。第二个声明创建一个特殊类型,它成为所有List&lt;SubClass1 of SomeClass&gt;的超类,并且List&lt;SubClass2 of SomeClass&gt;List&lt;SubClass3 of SomeClass&gt;

【讨论】:

  • 非常感谢,我今天学到了一些东西。
  • @hidralisk 很高兴您找到了有用的答案。
  • 非常感谢您的详细解释,但我想我还没有完全理解……看来我正处于启蒙的边缘。我将在即将到来的周末再次仔细阅读。再次感谢你。 :)
  • 我对答案进行了更新,添加了正在发生的事情的图形表示。希望它能更好地说明主题。
  • 我错过了通配符的定义。创建它是为了表示任何泛型类型的超类型。我可以从Oracle tutorial document 找到这句话:So what is the supertype of all kinds of collections? It's written Collection&lt;?&gt;. 有了你的回答和Oracle 教程,我终于明白为什么这两种泛型类型不同了。我真的很感激。 :)
猜你喜欢
  • 2013-09-08
  • 2019-03-31
  • 1970-01-01
  • 1970-01-01
  • 2020-02-14
  • 2011-01-10
  • 2016-07-01
  • 2013-08-08
相关资源
最近更新 更多