在Java中理解<? extends T>通配符困难

6

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

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

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

阅读关于“为什么不”的内容时,我理解了单词但没有理解概念:)
首先假设Fruit不是抽象类。我理解,因为我们处理多个子类型,所有这些类型都扩展自Fruit。据说由于我们无法确定水果的确切类型,所以我们无法将任何东西放入集合中。有几件事情我不明白:
1) 显然我们无法知道它是哪种水果,这让我感到困惑。在遍历集合时,我们难道不能通过typeof或其他instanceof检查来确定特定的类型吗?
2) 假设Fruit是一个具体类,为什么我们不能添加Fruit的实例呢?这似乎有道理,因为您至少会知道Fruit的API。即使您不知道Fruit的确切子类型,您也可以调用Fruit()上的标准方法。
我觉得这应该很明显,但是某些地方对我来说不太一样。感谢任何帮助。谢谢!

1
应该是new Apple()(即括号丢失)吧? - 5ar
可能是 http://stackoverflow.com/questions/12292752/java-wildcards-and-generics-super-t-and-extends-t?rq=1 的重复问题。 - adeady
可能是Java通配符和泛型? super T和? extends T的重复问题。 - adeady
2个回答

12

理解这个最好的方法是把通配符看作是在说列表而不是水果。换句话说:

List<Banana> allBananas = getMyBananas();
enumerateMyFruit(allBananas);

static void enumerateMyFruit(List<? extends Fruit> myFruit) {
    for (Fruit fruit : myFruit)
        System.out.println(fruit);
}
当我们将allBananas传递给enumerateMyFruit时,方法内部会丢失关于列表原始声明类型的信息。在这个例子中,我们可以清楚地看到为什么我们不应该能够将苹果放入List<? extends Fruit>中,因为我们知道该列表实际上是List<Banana>。再次强调,通配符告诉我们有关列表声明类型的一些信息。 List<? extends Fruit>应该被解读为“一个最初声明为包含Fruit或某个Fruit子类型的列表,但我们不知道那个声明类型是什么”。我们所知道的是,我们从列表中取出的所有东西都是Fruit
此外,你是正确的,我们可以迭代列表并使用instanceof来找出列表里真正的内容,但这不会告诉我们列表的原始声明类型。在上面的代码片段中,我们会发现列表中的所有东西都是Banana,但我也可以像如下声明allBananasList<Fruit>
你可能还需要看一下为什么List<Dog>不是List<Animal>,这解释了其中一些内容。通配符是我们在泛型类型中具有协变性的方式。 List<Dog>不是List<Animal>,但它是一个List<? extends Animal>。这带来了一个限制,即我们无法向List<? extends Animal>中添加元素,因为它可能是List<Dog>List<Cat>或其他什么东西。我们不再知道它是什么。
还有一个反义词? super,你可以看一下相关内容。我们可以将Fruit存储在List<? super Fruit>中,但我们不知道我们会从中取出什么样的对象。它的原始声明类型实际上可能是一个包含各种其他东西的List<Object>

3
首先要记住的是,对于没有通配符的泛型参数,你不能将一个替换为另一个。如果一个方法使用了 List<Fruit>,那么它就不会接收 List<Apple>,必须是完全匹配的。同时请记住这是关于变量的静态类型,与其内容没有直接联系。即使你的 List<Fruit> 包含所有的苹果,你仍然不能将其替换为 List<Apple>
所以我们正在谈论类型声明,而不是集合中的内容。
还要记住的是,instanceof 是在运行时进行的,而泛型是在编译时工作的。泛型的目的是帮助编译器确定事物的类型,这样你就不必使用 instanceof 和强制转换。
当一个方法 foo 接收一个带有泛型类型 List<? extends Fruit> 的参数时,这意味着该方法可以接受一系列类型,在这种情况下,这些类型可以是以下任何一种:
- 你可以传递一个 List<Fruit> - 你可以传递一个 List<Banana> - 你可以传递一个 List<Apple> (等等,对于你拥有的任何 Fruit 的子类型)
因此,你的方法可以处理这些任意元素的列表,但是方法体必须对它们都有效。当你将一个 Apple 添加到列表中时,那对于传入的是 List<Apple> 的情况是有效的,对于 List<Fruit> 也是有效的,但对于 List<Banana> 就不太行了。(并且使 Fruit 具体化也无济于事,添加一个 Fruit 对于 List<Apple> 没有作用。)
这就是为什么有一个规则,即任何时候通配符类型要扩展某些东西,添加东西都是不可能的,因为没有办法使其适用于可以传入的所有可能类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接