为什么 Java 中的 List.of() 不返回类型化的不可变列表?

Why does List.of() in Java not return a typed immutable list?

提问人:Johannes Rabauer 提问时间:9/13/2019 最后编辑:Mark RotteveelJohannes Rabauer 更新时间:10/18/2023 访问量:10848

问:

Java 中方法返回的列表确实返回了一个不可变列表,但通过查看创建的列表,这根本不可见。创建的列表只是抛出一个异常,而不是根本不显示更改列表的可能性。 我的观点是,那应该返回一个 that .这样,用户可以决定他是否愿意展示这个不变的事实。List.of(E... elements)List.of(E... elements)ImmutableListextends List

但我没有发现有人抱怨或展示替代解决方案。甚至 Guava 和 Apache Commons 默认也不会这样做。只有 Guava 提供了创建它的可能性(尽管有很多代码):

List<String> list = new ArrayList<String>(Arrays.asList("one", "two", "three"));
ImmutableList<String> unmodifiableList = ImmutableList.<String>builder().addAll(list).build();

但即使是这个类也有一个(已弃用的)和方法。addremove

谁能告诉我为什么没有人关心这个(看似根本的)问题?

爪哇岛

评论

6赞 Stultuske 9/13/2019
还行。。为什么你认为它应该?为什么你认为,如果某件事没有按照你想象的方式工作,那就是错误的或坏的?
3赞 Thomas 9/13/2019
Well 是一个标准的 JDK 类,同时是 Guava 等第三方库的一部分。为什么要返回这样的列表?List.of(...)ImmutableListList.of(...)
6赞 luk2302 9/13/2019
扩展 List 的不可变集合都打破了 Liskov 替换原则 - 它们声明的返回类型不会改变这一点,它一开始就被破坏了,并且没有真正的方法来修复它(不改变实际是什么)List
5赞 MikeFHay 9/13/2019
“(尽管有很多代码)”好消息是,你不需要所有这些代码,或者做同样的事情。ImmutableList.copyOf(list)ImmutableList.of("one", "two", "three")

答:

4赞 Kayaman 9/13/2019 #1

我想说的是,由于通常集合倾向于(或至少应该)被视为“默认不可变”(这意味着您很少修改未创建的集合),因此指定“这是不可变的”并不是很重要。指定“如果您愿意,可以安全地修改此集合”会更有用。

其次,您建议的方法行不通。您无法扩展和隐藏方法,因此唯一的选择是使其返回不是 的子类型的 。这将使它变得毫无用处,因为它需要一个新的接口,并且任何现有代码都无法使用它。ListImmutableListListImmutableList

那么这是最佳设计吗?不,不是真的,但对于向后兼容性,这不会改变。

4赞 MikeFHay 9/13/2019 #2

从所有 Collection 类型中删除 、 等并创建子接口 , 将使 Collection 接口的数量增加一倍,这是需要考虑的复杂性成本。此外,Collections 没有明确地分为 Mutable 和 Immutable:支持 ,但不支持 。addremoveMutableCollectionMutableListMutableSetArrays.asListsetadd

最终,需要权衡在类型系统中捕获多少,以及在运行时强制执行多少。理性的人可能会不同意在哪里划清界限。

28赞 Stuart Marks 9/13/2019 #3

这并不是说没有人在乎;这是一个相当微妙的问题。

没有“不可变”集合接口系列的最初原因是担心接口扩散。可能不仅有用于不可变性的接口,还可能有同步和运行时类型检查的集合,以及可以设置元素但不能添加或删除的集合(例如,Arrays.asList)或可以从中删除但不能添加元素的集合(例如,Map.keySet)。

但也可以说,不可变性是如此重要,以至于它应该是特例的,并且在类型层次结构中支持它,即使不支持所有这些其他特征。很公平。

最初的建议是让接口扩展为ImmutableListList

ImmutableList <:列表<:集合

(其中表示“是”的子类型”。<:

这当然可以做到,但随后会继承 的所有方法,包括所有 mutator 方法。必须对他们做点什么;子接口不能从超级接口“继承”方法。最好的办法是指定这些方法引发异常,提供这样做的默认实现,并可能将这些方法标记为已弃用,以便程序员在编译时收到警告。ImmutableListList

这有效,但没有多大帮助。这种接口的实现根本不能保证是不可变的。恶意或有缺陷的实现可能会覆盖 mutator 方法,或者它可能只是添加更多改变状态的方法。任何使用的程序都不能假设列表实际上是不可变的。ImmutableList

对此的变体是使其成为一个而不是一个接口,定义其赋值器方法以抛出异常,使它们成为最终值,并且不提供公共构造函数,以限制实现。事实上,这正是 Guava 的 ImmutableList 所做的。如果你信任 Guava 开发人员(我认为他们很有信誉),那么如果你有一个 Guava 实例,你就可以放心,它实际上是不可变的。例如,您可以将其存储在一个字段中,并知道它不会意外地从您下面改变。但这也意味着你不能添加另一个实现,至少在不修改 Guava 的情况下不能。ImmutableListImmutableListImmutableList

这种方法无法解决的一个问题是通过向上转换来“清理”不可变性。许多现有的 API 使用类型或 的参数来定义方法。如果将 an 传递给这样的方法,它将丢失指示列表不可变的类型信息。为了从中受益,您必须在任何地方添加不可变风味的重载。或者,您可以在所有位置添加检查。两者都很乱。CollectionIterableImmutableListinstanceof

(请注意,JDK 的 List.copyOf 回避了这个问题。即使没有不可变的类型,它也会在复制之前检查实现,并避免不必要地进行复制。因此,来电者可以用来制作防御性副本而不受惩罚。List.copyOf

作为替代方案,有人可能会争辩说,我们不想成为 的子接口,我们希望它成为一个超级接口:ImmutableListList

列表 <:ImmutableList

这样,就不必指定所有这些赋值器方法都抛出异常,它们根本不会出现在接口中。这很好,只是这个模型是完全错误的。既然是 ,那意味着也是 ,这显然是荒谬的。问题在于,“不可变”意味着对子类型的限制,这在继承层次结构中是无法做到的。相反,需要重命名它,以允许在层次结构向下移动时添加功能,例如,ImmutableListArrayListListArrayListImmutableList

列表<:ReadableList

哪个更准确。但是,与 .ReadableListImmutableList

最后,还有一堆我们没有考虑的语义问题。一个是关于不变性与不可修改性。Java 具有支持不可修改性的 API,例如:

List<String> alist = new ArrayList<>(...);
??? ulist = Collections.unmodifiableList(alist);

应该是什么类型?它不是一成不变的,因为如果有人更改后备列表,它就会改变。现在考虑:ulistalist

???<String[]> arlist = List.of(new String[] { ... }, new String[] { ... });

类型应该是什么?它当然不是不可变的,因为它包含数组,而数组始终是可变的。因此,完全不清楚说返回不可变的东西是否合理。List.of

评论

4赞 Louis Wasserman 9/14/2019
这是一个决定性的答案,因为你可以从做出决定的实际人之一那里得到“为什么”:)当然,你可以不同意这些原因,但这些就是原因。
1赞 Holger 9/18/2019
如果我们这样做了,为什么这些不可变的集合仍然没有优化的 NOR 实现 (JDK14-ea)?令人尴尬的是,最新、空间最优化的集合类型具有最差的流处理支持。我们可以进行直接的检查,就像不需要引入新的 API 一样,但它就是不起作用。forEachspliteratorlist.spliterator().hasCharacteristics(Spliterator.IMMUTABLE)NONULL
0赞 Martin Geisse 6/17/2023
"Thus, callers can use List.copyOf to make defensive copies with impunity" -- One thing that became clear to me only after reading this again is: If you think about build an API that takes an ImmutableList (e.g. Guava) as a parameter, you might as well take a List and make a List.copyOf(it). This will have a similar effect, but is more convenient to use and takes care of performance (share vs. copy) itself. The only downside is that informal docs are needed to emphasize that the list is copied, not shared, but sharing a list is rarely a good mode of operation for an API anyway.