如何向通配符泛型集合中添加元素?

21

为什么我在这段Java代码中会得到编译错误?

1  public List<? extends Foo> getFoos()
2  {
3    List<? extends Foo> foos = new ArrayList<? extends Foo>();
4    foos.add(new SubFoo());
5    return foos;
6  }

'SubFoo'是一个实现Foo接口的具体类,而Foo是一个接口。

使用这段代码我得到的错误:

  • 第3行: "Cannot instantiate ArrayList<? extends Foo>"
  • 第4行: "The method add(capture#1-of ? extends Foo) in the type List<capture#1-of ? extends Foo> is not applicable for the arguments (SubFoo)"

更新: 感谢Jeff C的建议,我可以将第3行改为"new ArrayList<Foo>();"。但是我仍然遇到第4行的问题。

5个回答

33

请改用这个:

1  public List<? extends Foo> getFoos()
2  {
3    List<Foo> foos = new ArrayList<Foo>(); /* Or List<SubFoo> */
4    foos.add(new SubFoo());
5    return foos;
6  }

一旦您将foos声明为List<? extends Foo>,编译器就不知道添加SubFoo是否安全。如果foos被分配了ArrayList<AltFoo>会怎么样?这是一个有效的赋值,但添加SubFoo会污染集合。


谢谢,我试了一整天也没想出来。 - Quan Bui

20

我想给这个旧的线程添加一些内容,总结一下使用类型实例化或通配符实例化的列表参数的属性...

当一个方法有一个参数/返回结果是一个List时,使用类型实例化或通配符实例化决定:

  1. 可以作为参数传递给方法的List类型
  2. 可以从方法结果中填充的List类型
  3. 可以在方法内部写入到List的元素类型
  4. 可以从方法读取元素时填充的类型

参数/返回类型: List< Foo>

  1. 可以作为参数传递给方法的List类型:
    • List< Foo>
  2. 可以从方法结果中填充的List类型:
    • List< Foo>
    • List< ? super Foo>
    • List< ? super SubFoo>
    • List< ? extends Foo>
    • List< ? extends SuperFoo>
  3. 可以在方法内部写入到List的元素类型:
    • Foo及其子类型
  4. 可以从方法读取元素时填充的类型:
    • Foo及其父类型(最高到Object

参数/返回类型: List< ? extends Foo>

  1. 可以作为参数传递给方法的List类型:
    • List< Foo>
    • List< Subfoo>
    • List< SubSubFoo>
    • List< ? extends Foo>
    • List< ? extends SubFoo>
  2. List< ? extends SubSubFoo>
  3. 可以从方法结果中填充的列表类型:
    • List< ? extends Foo>
    • List< ? extends SuperFoo>
    • List< ? extends SuperSuperFoo>
  4. 方法中可以写入列表的元素类型:
    • 无! 不可能添加。
  5. 在方法内读取元素时可以填充的类型:
    • Foo及其超类型(最多到Object

参数/返回类型:List<? super Foo>

  1. 可以作为参数传递给方法的列表类型:
    • List< Foo>
    • List< Superfoo>
    • List< SuperSuperFoo>
    • List< ? super Foo>
    • List< ? super SuperFoo>
    • List< ? super SuperSuperFoo>
  2. 可以从方法结果中填充的列表类型:
    • List< ? super Foo>
    • List< ? super SubFoo>
    • List< ? super SubSubFoo>
  3. 在方法内写入列表的元素类型:
    • Foo及其超类型
  4. 在方法内读取元素时可以填充的类型:
    • Foo及其超类型(最多到Object

解释/评论

  • 外部调用者的需求驱动方法声明的设计,即公共API(通常是主要考虑因素)
  • 内部方法逻辑的需求驱动有关实际数据类型的任何其他决策,包括在内部声明和构建(通常是次要考虑因素)
  • 如果调用方代码始终专注于操作Foo类,则使用List<Foo>,因为它最大限度地提高了读写的灵活性
  • 如果可能存在许多不同类型的调用者,专注于操作不同类(不一定是Foo)并且Foo类型层次结构中存在单个最上层的类,以及如果方法需要在内部写入列表而调用方列表操作进行读取,则使用List<? extends UpperMostFoo>。在这种情况下,方法可以在内部使用List< UpperMostFoo>并向其添加元素,然后返回List< ? extends UpperMostFoo>
  • 如果可能存在许多不同类型的调用者,专注于操作不同类(不一定是Foo),并且读写列表是必需的,并且Foo类型层次结构中存在一个最低级别的类,则使用List< ? super LowerMostFoo>是有意义的。

3
我认为“List<? super Foo>”中的第3和第4个选项是错误的。应该分别是“Foo及其子类型”和“Object”。请注意,这里不要有解释或其他额外内容。 - FuegoFro

7

尝试:

public List<Foo> getFoos() {
    List<Foo> foos = new ArrayList<Foo>();
    foos.add(new SubFoo());
    return foos;
}

通用的ArrayList构造函数需要有一个特定的类型来进行参数化,你不能在那里使用“?”通配符。将实例化更改为“new ArrayList<Foo>()”可以解决第一个编译错误。
'foos'变量的声明可以有通配符,但由于你知道精确的类型,因此引用相同的类型信息更有意义。现在所说的是foos保存了Foo的某个具体子类型,但我们不知道是哪一个。添加SubFoo可能是不允许的,因为SubFoo不是“所有Foo的子类型”。将声明更改为'List<Foo> foos ='可以解决第二个编译错误。
最后,我会将返回类型更改为'List<Foo>',因为该方法的客户端无法根据当前定义对返回值进行操作。你应该很少在返回类型中使用通配符。如果需要,使用参数化的方法签名,但最好将有界类型仅出现在方法参数中,因为这留给调用者自己决定它们可以传递特定类型并相应地操作它们。

2
我不同意你的最后一点。如果调用者只需要从列表中获取东西,通配符返回类型非常有用。 - newacct

3

为了了解泛型如何工作,请参考以下示例:

    List<SubFoo> sfoo = new ArrayList<SubFoo>();
    List<Foo> foo;
    List<? extends Foo> tmp;

    tmp = sfoo;
    foo = (List<Foo>) tmp;

问题在于,这并不是为本地/成员变量设计的,而是为函数签名设计的,这就是为什么它如此反常。


2
以下内容可以正常工作:
public List<? extends Foo> getFoos() {
    List<Foo> foos = new ArrayList<Foo>();
    foos.add(new SubFoo());
    return foos;
}

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