为什么在 java.utils 中枚举被转换为 ArrayList 而不是 List?

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

显然,ArrayListList,但我的印象是,返回接口类型而不是实现类型通常是一个好的实践。

3574 次浏览

当返回 List时,您将推广 接口程序,这是一个非常好的实践。然而,这种方法有其局限性。例如,您不能使用为 ArrayList定义的一些方法,而且 List接口中不存在这些方法-详细信息请参阅 这个答案

我引用的是《 JavaTM 教程》中的 API 设计:

... 返回一个 任何类型的对象,它是 工具延伸的一个集合接口。这可以是其中一个接口,也可以是扩展或实现这些接口之一的特殊用途类型。

..在某种意义上,返回值应该具有与输入参数相反的行为: 返回 最具体适用的集合接口,而不是最一般的的是 最好的。例如,如果确保始终返回 SortedMap,那么应该给相关方法返回类型为 SortedMap而不是 Map。构建 SortedMap实例比构建普通 Map实例更耗时,而且也更强大。既然您的模块已经投入了时间来构建 SortedMap,那么让用户访问其增强的功能是很有意义的。此外,用户还可以将返回的对象传递给需要 SortedMap的方法,以及接受任何 Map的方法。

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

在任何其他情况下,仍然可以这样写:

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

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

我同意将 自己的代码写入接口是正确的,但是如果要向第三方返回一个 易变的集合,重要的是要让第三方知道他们返回的是什么类型的 List

LinkedListArrayList在各种操作中的性能非常不同。例如,去掉 ArrayList的第一个元素是 O(n),但是去掉 LinkedList的第一个元素是 O(1)

通过完全指定返回类型,JDK 作者在明确的代码中是 传递额外的信息,关于他们返回给您的对象的类型,因此您可以编写代码来正确地使用这个方法。如果你真的需要一个 LinkedList,你知道你必须在这里指定一个。

最后,通过实现编写接口代码的主要原因是您认为实现将发生变化。JDK 作者可能认为他们是 永远不会,将要改变这个方法; 它永远不会返回 LinkedListCollections.UnmodifiableList。然而,在大多数情况下,你可能仍然会这样做:

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

返回新创建的可变对象的“独占所有权”的函数通常应该是最具体的实用类型; 那些返回不可变对象的函数,特别是如果它们可能被共享的话,通常应该返回不那么具体的类型。

区别的原因在于,在前一种情况下,一个对象总是能够产生一个指定类型的新对象,而且由于接收方将拥有该对象,并且不知道接收方可能希望执行什么操作,所以通常不会有办法让返回该对象的代码知道是否有任何替代接口实现可以满足接收方的需求。

在后一种情况下,对象是不可变的这一事实意味着该函数可能能够识别一个替代类型,这个替代类型可以完成更复杂类型可以完成的 给出了确切的内容的所有工作。例如,Immutable2dMatrix接口可以由 ImmutableArrayBacked2dMatrix类和 ImmutableDiagonal2dMatrix类实现。假设返回一个正方形 Immutable2dMatrix的函数可以决定返回一个 ImmutableDiagonalMatrix实例,如果主对角线外的所有元素碰巧为零,或者如果不是,返回一个 ImmutableArrayBackedMatrix。前一种类型会占用更少的存储空间,但是接收者不应该关心它们之间的差异。

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

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

在调用 var 接口方法而不是直接在对象上调用方法时,存在 很小开销。

这个开销通常不超过1或2个处理器指令。如果 JIT 知道方法是 final 方法,那么调用方法的开销就会更低。这对于你和我的大多数代码来说是不可测量的,但是对于 java.utils 中的低级方法来说,可能在某些代码中使用,这是一个问题。

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

显然,java.utils的作者没有办法知道所有调用 Collections.list ()的软件对结果做了什么,如果他们更改 Collections.list ()的植入,也没有办法重新测试这个软件。因此,即使类型系统允许,他们也不会更改 Collections.List ()的植入以返回不同类型的 List!

在编写自己的软件时,您(希望)已经进行了覆盖所有代码的自动化测试,并且很好地理解了代码之间的相互关系如何包括知道性能在哪里是一个问题。在软件设计发生变化的时候,能够在不更改调用者的情况下对方法进行更改是非常有价值的。

因此,这两组权衡是非常不同的。