“ArrayList<A>”和“ArrayList<? extends A>”之间的实际区别是什么?

5
我看了一下stackoverflow上的问题q1q2q3,但它们并没有完全解答我的问题。
请注意,ArrayList<A>ArrayList<? extends A> 用于声明变量或参数(不是用于创建一个新的泛型类)。
在声明对象属性时(情况1),这两个表达式是否等效?
class Foo {

  private ArrayList<A> aList; // == ArrayList<? extends A> aList;
}

编辑:在允许添加到 aList 的对象类型方面,这两个表达式是否等价?但是与以下情况不同吗?

但是当用于参数声明(情况2)时它们是不同的吗?

void methodFoo(ArrayList<A> al)  !=  void methodFoo(ArrayList<? extends A> al)

因为第一个只允许传递ArrayList对象,而第二个则可以"更宽松地"允许发送ArrayList<A1>ArrayList<A2>(只要A1和A2扩展了A)?
如果这是正确的,是否还有其他情况下这两个表达式是有效不同的呢?
谢谢,

据我所知,没有任何区别。 - Martijn Courteaux
6个回答

8

让我们看几个实际的例子。比如,你有:

List<Number> list;

这意味着无论分配给此变量或字段的内容都应该采用Number并输出Number,因此您始终知道可以期待什么。由于Integer扩展了Number,因此可以将其添加到此列表中。但是,您不能将例如ArrayList<Long>分配给此列表。
但请考虑以下情况:
List<? extends Number> list;

这篇文章说:嘿,这是一个扩展了Number的某种列表,但没人知道具体是什么。这意味着什么?这意味着你可以赋予这个列表一个ArrayList<Long>,而在第一个情况中你不能这样做。你仍然知道这个列表输出的任何东西都将是一个Number,但你不再能够把一个Integer放进去了。
还有相反的情况:
List<? super Number> list;

通过打印您所说的内容:这是一个Number或其超类列表。这就是一切变得相反的地方。现在,该列表可以引用ArrayList<Object>ArrayList<Number>。现在我们不知道此列表将会输出什么。它将是一个Number吗?还是一个Object?但是现在我们知道,我们可以将Number以及IntegerLong等任何Number子类放入此列表中。

顺便说一下,有一个规则,它称为生产者使用extends,消费者使用super(简称PECS)。如果需要列表输出值,则为生产者,这是第二种情况。如果需要列表接受值,则为消费者,这是第三种情况。如果两者都需要,请勿使用通配符(这是第一种情况)。

我希望这能澄清问题。


@Malcom:对于可以或不可以添加到list中的内容,你解释得很好。那么第二种情况呢:一个方法可以接受哪些类型的列表? - cibercitizen1
如我在回答中所说,是某些扩展Number的东西的列表。它可能是类似于List<Long>List<Integer>之类的东西。因此你知道这个列表会给你一个Number,但你不知道你可以放什么在里面。 - Malcolm

4
这将解释它们之间的不同之处:
public class GenericsTest {
  private ArrayList<A> la;
  private ArrayList<? extends A> lexta;

  void doListA(ArrayList<A> la) {}
  void doListExtA(ArrayList<? extends A> lexta) {}

  void tester() {
    la = new ArrayList<SubA>(); // Compiler error: Type mismatch
    doListA(new ArrayList<SubA>());  // Compiler error: Type mismatch
    lexta = new ArrayList<SubA>();
    doListExtA(new ArrayList<SubA>());
  }

  static class A {}
  static class SubA extends A {}
}

正如您所看到的,调用方法和分配变量/实例字段具有相同的规则。将方法调用视为将参数分配给其声明的参数。


1

ArrayList<A> 表示特定的 A 类,而 ArrayList<? extends A> 表示类 A 或任何继承 A 的类(A 的子类),这使其更加通用。


那并不是真的。你可以有一个List<Number>,在其中添加Integer - Martijn Courteaux
ArrayList<A> 是错误的,也不是他问题的答案。他问的是在他提供的例子中这些表达式有什么不同。 - Shehzad

1

使用private ArrayList<A> aList;作为变量声明并不真正等同于使用通配符private ArrayList<? extends A> aList;

通配符版本将允许您分配任何扩展A和A本身类型的ArrayLists,但会拒绝向列表添加元素,因为它无法确定是否类型安全。另一方面,使用ArrayList<A>,您只能分配类型为A的ArrayLists(或ArrayList的扩展),然后可以向其添加A元素和任何扩展A的元素。

FYI:您应该优先使用更抽象的类型来声明变量/参数,例如List<A>Collection<A>


这不是他问题的答案。他问的是在他提供的例子中这些表达式有什么不同。 - Shehzad
好的,那么这个问题对我来说有点不清楚。无论如何,似乎cibercitizen1做出了ArrayList<A>和ArrayList<? extends A>等价的假设。但事实并非如此。 - nansen

0

一开始很难理解,但是泛型中不适用继承,即如果B扩展A,则List<B>不是List<A>的“子类”(不能分配给它)。此外,List<? extends A>也不是List<A>的“子类”(不能分配给它)。


1
是的,两种方法都必须处理异常。如果你有一个 List<A>,并不意味着你不能把一个 B 放进去。 - Malcolm

0

主要的区别在于,如果泛型形式被用作基类(或接口)中方法的参数或返回类型,它允许更广泛的类型签名被视为覆盖而不是重载。

Java中的函数覆盖和重载

例如,以下代码在Java 7中是合法的:

interface A
{
    List<? extends Number> getSomeNumbers();
}

class B implements A
{
    @Override
    public ArrayList<Integer> getSomeNumbers()
    {
        return new ArrayList<>();
    }
}

Enumeration<? extends ZipEntry>和Enumeration<ZipEntry>之间的区别是什么?

所有这些意味着,有时候你可以编写一些代码,当其他人使用它时需要较少的强制转换。这不仅可以减少他们需要输入的内容,还可以消除可能的故障。

当然,Java泛型的问题在于它们是以向后兼容的方式引入的。因此,并非所有您认为应该工作的东西都能正常工作,而且关于到底哪些东西能够正常工作,哪些不能工作的细节变得非常复杂。

http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ812


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