java 中的不可变类型(尤其是集合)和协方差

Immutable types (especially collections) and covariance in java

假设我有这个 class WidgetHolder 包含 Widget 个实例。在内部,我用 List<Widget> 来支持它。我保证它是不可变的,所以在构建之后没有小部件被添加到持有者中。

WidgetHolder 还公开了一个 getWidgets() 操作,该操作 return 是小部件列表的不可变视图。

我想要一个包含 SpecificWidget 个实例的派生 SpecificWidgetHolder class,getWidgets() 的 return 类型是 Collection<SpecificWidget>.

我有同样的保证,即 SpecificWidgetHolder 构造函数采用 SpecificWidget 个实例的集合,并且不会以任何方式修改它。

鉴于 WidgetHolderSpecificWidgetHolder 都是不可变的,证明泛型不变性的常见反对意见似乎并不适用 (?)

在 Java 中是否有一种简洁的表达方式?

谢谢!

试试这个:

public class WidgetHolder<T extends Widget>
{
  public List<T> getWidgets()
  {
    return (widgets);
  }

  private List<T> widgets;

} // class WidgetHolder

public class SpecificWidgetHolder extends WidgetHolder<SpecificWidget>
{
}

您描述的场景很常见:您在内部存储了一个对象集合,并希望公开该集合的只读 版本。这通常使用 Collections#unmodifiableCollection 方法(以及其他集合类型的特定方法)来完成:

class WidgetHolder {
    private final List<Widget> widgets = new ArrayList<Widget>();

    public List<Widget> getWidgets() {
        return Collections.unmodifiableList(widgets);
    }
}

但请注意,如果集合无论如何都是不可修改的,您可以通过利用集合不可修改的事实来实现所需的协方差。当不可修改时,可以将return类型声明为

public List<? extends Widget> getWidgets() { ... }

这样大家就可以像以前一样使用列表了。他可以阅读和迭代它。他不能添加新元素,因为它是不可修改的 - 一个很好的副作用是这种不可修改性(在某种程度上)在这里可见 syntactically:

List<Widget> widgets = widgetHolder.getWidgets();
widgets.add(new Widget()); // throws UnsupportedOperationException AT RUNTIME!

对比

List<? extends Widget> widgets = widgetHolder.getWidgets();
widgets.add(new Widget()); // Does not compile! 

(请注意,像 widgets.remove(x)widgets.add(null) 这样的调用仍然可以编译,但会抛出一个 UnsupportedOperationException - 这只是一个微妙的提示)


关于您的问题,关键是您可以使用更具体的 return 类型覆盖此方法。 这里是真正的协方差

因此,在 class SpecificWidgetHolder 中,您可以选择 3 种覆盖此方法的方法:

  1. 使用与 superclass 相同的类型:

    public List<? extends Widget> getWidgets()
    
  2. 使用特定类型,其中你知道它是可赋值给super的类型class:

    public List<SpecificWidget> getWidgets()
    
  3. 再次使用通配符类型,为进一步特化留下空间(如 VerySpecificWidgetHolder):

    public List<? extends SpecificWidget> getWidgets()
    

class 的使用方式存在细微差别。特别是,有关 "specificness" 的信息是否可供调用者使用。我试图在这个例子中指出这一点:

// With option 1, you only receive Widgets - even
// from a SpecificWidgetHolder
List<? extends Widget> widgets = specificWidgetHolder.getWidgets();
Widget widget = widgets.get(0);

// With option 3., you still know that a SpecificWidgetHolder
// provides a list of SpecificWidgets
List<? extends SpecificWidget> specificWidgets = specificWidgetHolder.getWidgets();
SpecificWidget specificWidget = specificWidgets.get(0);

基本上,您必须决定拥有 SpecificWidgetHolder 的人是否应该能够从中获得 SpecificWidget 个实例。


旁注:如另一个答案所示,这也可以用泛型来解决。但我认为这里没有必要。而且我通常对将类型参数附加到 classes 有点犹豫,因为它们可能比非泛型类型更难理解和维护。泛型(类型参数)就像盐:在正确的位置和正确的剂量下,它们可以使事情变得简单更好。但是当你被它们冲昏头脑,并最终得到像 <S, T extends Number> Foo<S, ? super T> getFoo() 这样的嵌套泛型和方法签名时,这可能会大大阻碍可读性和可维护性