为什么在Java中,枚举类型会被转换成ArrayList而不是List?

74

java.utils包中的Collections.list()方法返回ArrayList<T>而不是List<T>,这是否有充分的理由呢?

显然,ArrayList是一个List,但我认为通常最好返回接口类型而不是实现类型。


从Javadoc中:"返回一个数组列表[...]"。为什么他们决定锁定API以返回ArrayList是未知的,即使查看Collections的源代码,我们也无法回答这个问题。 - Laf
可能是因为需要维护枚举的顺序?在你提问中的链接中有提到,_返回一个包含由指定枚举返回的元素的数组列表,按照枚举返回它们的顺序排列_。 - Amit.rk3
5
无论您选择哪种实现方式,List 都会始终保持其顺序。 - Jib'z
@Amit.rk3 一个List保留了元素添加的顺序,或者至少这是由List接口定义的一般契约:“_有序集合(也称为序列)_”。 - Laf
@Amit.rk3 还要注意,枚举(以及它所代表的小写字母“e”)与enum不同。它们共享一个名称,但没有其他关联。 - yshavit
4个回答

54

免责声明:我不是JDK的作者。

我同意编写你自己的代码来实现接口的做法是正确的,但是如果你要向第三方返回一个可变的集合,就很重要让第三方知道他们得到了什么样的List

LinkedListArrayList在性能上有很大的差异,对于各种操作而言都如此。例如,从ArrayList中删除第一个元素是O(n),而从LinkedList中删除第一个元素是O(1)

通过完全指定返回类型,JDK作者正在用明确的代码传递额外的信息,告诉你他们正在返回什么样的对象,这样你就可以编写正确使用该方法的代码。如果你真的需要一个LinkedList,那么你知道你必须在这里指定一个。

最后,选择面向接口而不是具体实现的主要原因是如果你认为实现会变化。JDK作者可能认为他们永远不会更改此方法; 它永远不会返回LinkedListCollections.UnmodifiableList。不过,在大多数情况下,你可能仍然会这样做:

List<T> list = Collections.list(enumeration);

19
这似乎是API设计中的一种Postel法则:在输入方面要通用,在输出方面要具体。 - Tianxiang Xiong
2
总有一天他们会添加一个 toLinkedList 方法,然后 ArrayList 版本的名称 list 看起来就很傻了。 - user253751
1
我认为这里重要的信息是列表是否可变。例如,接口的哪些方法会抛出UnsupportedOperationException,哪些不会。 - keuleJ

32
当返回 List 时,您将会使用“面向接口编程”的方法,这是一种非常好的实践。但是,这种方法也有其限制。例如,您不能使用一些仅适用于 ArrayList 而在 List 接口中不存在的方法 - 详情请参见 this answer
我引用了 Java™ 教程中的API 设计章节。
...可以返回任何实现或扩展集合接口之一的任何类型的对象。这可以是其中一个接口或扩展或实现这些接口的特殊用途类型。
从某种意义上讲,返回值应该具有与输入参数相反的行为:最好返回最具体适用的集合接口,而不是最通用的。例如,如果您确信始终会返回SortedMap,则应将相关方法的返回类型设置为SortedMap而不是Map。SortedMap实例比普通Map实例更耗时构建,且功能更强大。鉴于您的模块已经投入了时间来构建SortedMap,因此将其增加的功能提供给用户是明智的选择。此外,用户将能够将返回的对象传递给需要SortedMap的方法,以及那些接受任何Map的方法。

由于 ArrayList 本质上是一个数组,当我需要一个“集合-数组”时,它们是我的首选。因此,如果我想将枚举转换为列表,我的选择将是一个数组列表。

在任何其他情况下,仍然可以编写以下内容:

List<T> list = Collections.list(e);

4
关于“选择最具体适用的集合接口而非最通用的接口”,这是有道理的,但ArrayList不是一个接口。在这种情况下,List似乎是最具体的接口。 - Tianxiang Xiong
2
SortedMap的情况下,它为普通的Map添加了功能,因此我认为将其指定为返回类型是有意义的。然而,ArrayListList没有任何增加,可以与LinkedList交换,你不会看到任何不同(在功能上而非性能上)。尽管如此,我认为您的参考非常有趣。+1。 - Laf
1
@Laf 实际上,在选择LinkedList或ArrayList时有许多要考虑的因素,例如获取时间复杂度等。 - Maroun
5
强调一下:这应该是一个设计决策,而不是“巧合”。如果你在内部存储数据时使用了SortedMap,你不应该因为它“就是”一个SortedMap就直接返回它。你应该决定调用者是否需要知道这是一个SortedMap。同时要注意,一旦你返回了一个SortedMap,你将永远无法将其泛化回普通的Map——而另一个方向(变得更加具体,并返回一个SortedMap,其中最初只返回了一个Map)则始终是可能的。 - Marco13

6
返回结果如下:

返回“独占所有权”的新创建的可变对象的函数应该尽可能具体;返回不可变对象的函数,特别是如果它们可能被共享,应该返回较不具体的类型。

之所以要区分这两种情况,是因为在前一种情况下,对象将始终能够生成指定类型的新对象,并且由于接收方将拥有该对象并且无法确定接收方可能希望执行什么操作,因此通常没有办法让返回对象的代码知道任何替代接口实现是否能够满足接收方的需求。

在后一种情况下,对象是不可变的事实意味着函数可以识别出一个替代类型,该替代类型可以在给定其确切内容的情况下完成比更复杂的类型更多的任务。例如,Immutable2dMatrix接口可以由ImmutableArrayBacked2dMatrix类和ImmutableDiagonal2dMatrix类实现。一个应该返回一个正方形Immutable2dMatrix的函数可以决定返回一个ImmutableDiagonalMatrix实例,如果除了主对角线外的所有元素都为零,或者一个ImmutableArrayBackedMatrix如果不是。前者需要更少的存储空间,但接收方不应关心它们之间的区别。

返回Immutable2dMatrix而不是具体的ImmutableArrayBackedMatrix允许代码根据数组包含的内容选择返回类型;这也意味着如果应该返回数组的代码恰好持有适当的Immutable2dMatrix实现,则可以简单地返回该实现,而无需构造新实例。在使用不可变对象时,这两个因素都可能是重要的“胜利”。

然而,在使用可变对象时,这两个因素都不起作用。可变数组生成时可能没有任何元素超出主对角线,但这并不意味着它永远不会有这样的元素。因此,虽然ImmutableDiagonalMatrix实际上是Immutable2dMatrix的子类型,但MutableDiagonalMatrix不是Mutable2dMatrix的子类型,因为后者可以接受超出主对角线的存储,而前者不能。此外,尽管不可变对象通常可以共享且应该共享,但可变对象通常不能共享。请求使用特定内容初始化的新可变集合的函数将需要创建一个新集合,无论其支持存储是否与所请求的类型匹配。


1

在接口上调用方法相比直接在对象上调用方法会带来一些轻微的开销,但通常不超过1或2个处理器指令。如果JIT知道该方法是final,则调用方法的开销甚至更低。对于大多数代码而言,这种开销无法测量,但对于java.utils中的低级方法可能会在某些代码中使用时成为问题。

此外,正如其他答案所指出的那样,返回对象的具体类型(即使被隐藏在接口后面)也会影响使用它的代码的性能。这种性能变化可能非常大,以至于调用软件无法正常工作。

显然,java.utils的作者无法知道调用Collections.list()的所有软件都对结果执行了什么操作,也无法在更改Collections.list()的实现后重新测试此软件。因此,即使类型系统允许,他们也不会更改Collections.list()的实现以返回不同类型的列表!

当你编写自己的软件时,(希望)你有自动化测试来覆盖所有代码,并且很好地了解代码之间的相互关系,包括知道性能存在问题的位置。在软件设计变化时,能够修改方法而不必更改调用者的能力具有很大的价值。
因此,这两组权衡非常不同。

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