苦苦挣扎于理解<?

Struggling with understanding <? extends T> wildcard in Java

我有一个非常基本的问题。

下面的代码不编译(假设苹果扩展了水果):

1
2
    List<? extends Fruit> numbers = new ArrayList<>();
    numbers.add(new Apple());  //compile time error

当我读到"为什么"时,我理解的是单词,而不是概念。

假设第一个水果不是抽象类。我理解,因为我们处理的是多个子类型,所有这些子类型都扩展了水果。因为我们不能准确地说出水果的种类,所以我们不能把任何东西放进收藏品中。有几件事我不明白:

1)显然我们不知道是哪种水果让我困惑。在迭代集合时,我们是否能够通过typeof或其他instanceof check来分辨特定的类型?

2)假设水果是一个具体的类,为什么不允许我们添加水果实例?这似乎是有道理的,因为你至少知道水果的API。即使您不知道fruit的确切子类型,至少可以在fruit()上调用标准方法。

我觉得这应该是相当明显的,但有些东西不适合我。任何帮助都是感激。谢谢!


理解这一点的最好方法是将通配符视为对列表的某种说明,而不是水果。换句话说:

1
2
3
4
5
6
7
List<Banana> allBananas = getMyBananas();
enumerateMyFruit(allBananas);

static void enumerateMyFruit(List<? extends Fruit> myFruit) {
    for (Fruit fruit : myFruit)
        System.out.println(fruit);
}

当我们将allBananas传递给enumerateMyFruit时,在方法内部我们会丢失关于列表原始声明类型的信息。在这个例子中,我们可以非常清楚地看到为什么我们不能把苹果放在一个List中,因为我们知道这个列表实际上是一个List。同样,通配符告诉我们一些关于列表声明类型的信息。

List应该理解为"一个最初声明保存FruitFruit的某个子类型的列表,但我们不知道该声明的类型是什么"。我们所知道的是,我们从名单中拿出的所有东西都是一个Fruit

而且,您是对的,我们可以迭代列表并使用instanceof来找出列表中的实际内容,但这不会告诉我们列表的原始声明类型。在上面的代码片段中,我们会发现列表中的所有内容原来都是一个Banana,但我可以像List一样轻松地声明allBananas

你也可以看到为什么一个List不是一个List,这解释了其中的一些。通配符是我们如何在泛型类型之间有协方差的。List不是List而是List。这是因为我们不能在List中添加限制,因为它可能是ListList或其他东西。我们不知道了。

还有一个是? super,正好相反。我们可以把Fruit存储在List中,但我们不知道从中取出什么样的物体。它最初声明的类型实际上可能是一个List,其中包含各种各样的其他内容。


首先要记住,对于没有通配符的泛型参数,不能用一个替换另一个。如果一个方法取一个List,它不会取一个List,它必须是完全匹配的。还要记住,这是关于变量的静态类型,没有直接的内容连接。即使你的List包含所有的苹果,你仍然不能用它来代替List。所以我们讨论的是类型声明,而不是集合中的内容。

还请记住,instanceof是在运行时完成的,泛型在编译时工作。泛型是帮助编译器找出什么类型的东西,所以您不必求助于instanceof和casting。

当一个方法foo接受一个泛型类型为List的参数时,这是一种表示该方法可以接受一系列类型的方法,在这种情况下,这些类型可以是以下任何一种类型:

  • 你可以通过一个List

  • 你可以通过一个List

  • 你可以通过一个List

  • 小精灵

    (等,对于您拥有的Fruit的任何子类型)

    所以您的方法可以使用其中任何一个的列表,但是方法体必须对其中任何一个有效。当你把一个Apple添加到列表中时,这适用于传入的是List,它适用于List,适用于List,没有那么多。(制作水果混凝土也没什么帮助,添加Fruit也不适用于List案例。)

    这就是为什么有一个规则,即每当通配符类型扩展某个内容时,添加内容是不可能的,它无法适用于所有可以传入的可能类型。