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
个实例的集合,并且不会以任何方式修改它。
鉴于 WidgetHolder
和 SpecificWidgetHolder
都是不可变的,证明泛型不变性的常见反对意见似乎并不适用 (?)
在 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 种覆盖此方法的方法:
使用与 superclass 相同的类型:
public List<? extends Widget> getWidgets()
使用特定类型,其中你知道它是可赋值给super的类型class:
public List<SpecificWidget> getWidgets()
再次使用通配符类型,为进一步特化留下空间(如 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()
这样的嵌套泛型和方法签名时,这可能会大大阻碍可读性和可维护性
假设我有这个 class WidgetHolder
包含 Widget
个实例。在内部,我用 List<Widget>
来支持它。我保证它是不可变的,所以在构建之后没有小部件被添加到持有者中。
WidgetHolder
还公开了一个 getWidgets()
操作,该操作 return 是小部件列表的不可变视图。
我想要一个包含 SpecificWidget
个实例的派生 SpecificWidgetHolder
class,getWidgets()
的 return 类型是 Collection<SpecificWidget>
.
我有同样的保证,即 SpecificWidgetHolder
构造函数采用 SpecificWidget
个实例的集合,并且不会以任何方式修改它。
鉴于 WidgetHolder
和 SpecificWidgetHolder
都是不可变的,证明泛型不变性的常见反对意见似乎并不适用 (?)
在 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 种覆盖此方法的方法:
使用与 superclass 相同的类型:
public List<? extends Widget> getWidgets()
使用特定类型,其中你知道它是可赋值给super的类型class:
public List<SpecificWidget> getWidgets()
再次使用通配符类型,为进一步特化留下空间(如
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()
这样的嵌套泛型和方法签名时,这可能会大大阻碍可读性和可维护性