Java 8为什么不包含不可变的集合?


130

Java团队做了很多出色的工作,消除了Java 8中函数编程的障碍。特别是,对java.util集合的更改在将转换链接到非常快速的流式操作方面做得很好。考虑到他们在集合上添加一流的函数和函数方法做得多么出色,为什么他们完全无法提供不可变的集合甚至不可变的集合接口呢?

在不更改任何现有代码的情况下,Java团队可以随时添加与可变接口相同的不可变接口,减去“ set”方法并使现有接口从其扩展,如下所示:

                  ImmutableIterable
     ____________/       |
    /                    |
Iterable        ImmutableCollection
   |    _______/    /          \   \___________
   |   /           /            \              \
 Collection  ImmutableList  ImmutableSet  ImmutableMap  ...
    \  \  \_________|______________|__________   |
     \  \___________|____________  |          \  |
      \___________  |            \ |           \ |
                  List            Set           Map ...

当然,诸如List.add()和Map.put()之类的操作当前会返回给定键的布尔值或先前值,以指示该操作是成功还是失败。不可变集合必须将此类方法视为工厂,并返回包含添加元素的新集合-该集合与当前签名不兼容。但这可以通过使用其他方法名称(例如ImmutableList.append()或.addAt()和ImmutableMap.putEntry())来解决。由此产生的冗长性将远远超过使用不可变集合的好处所抵消,并且类型系统将防止调用错误方法的错误。随着时间的流逝,旧的方法可能会被弃用。

永恒收藏的胜利:

  • 简单性-当基础数据不变时,关于代码的推理就更简单了。
  • 文档-如果某个方法采用不可变的集合接口,则您将知道它不会修改该集合。如果某个方法返回一个不可变的集合,则说明您无法对其进行修改。
  • 并发-不可变集合可以在线程之间安全地共享。

作为尝过假定不变性的语言的人,很难回到猖mutation的突变的狂野西部。Clojure的集合(序列抽象)已经具有Java 8集合提供的所有功能以及不可变性(尽管由于同步的链表而不是流,因此可能会使用额外的内存和时间)。Scala具有可变的集合和不可变的集合,具有完整的操作集,尽管这些操作很急切,但调用.iterator可以提供一个惰性视图(还有其他惰性评估它们的方法)。我看不出没有不可变的集合,Java如何能够继续竞争。

有人可以指出我的历史或讨论吗?当然,它在某个地方是公开的。


9
与此相关-Ayende最近在博客中发布了带有基准的C#中的集合和不可变集合。ayende.com/blog/tags/performance-tl ; dr-不变性很
Oded 2013年

20
根据您的层次结构,我可以给您一个ImmutableList,然后在不希望它会破坏很多事情的情况下在您身上更改它,因为您只有const收藏

18
@Oded不可变性很慢,但锁定也很慢。保持历史也是如此。在许多情况下,简单/正确是值得的。对于小型收藏,速度不是问题。Ayende的分析基于这样的假设:您不需要历史记录,锁定或简单性,并且您正在使用大型数据集。有时候这是对的,但这并不是一件总会更好的事情。需要权衡。
GlenPeterson 2013年

5
@GlenPeterson是防御性副本Collections.unmodifiable*()的用途。但不要将它们视为不可变的
棘手怪胎

13
嗯 如果您的函数ImmutableList在该图中采用,人们可以传递可变的List?不,这是一个非常糟糕的违反LSP的。
Telastyn

Answers:


113

因为不可变的集合绝对需要共享才能使用。否则,每个操作都会将其他整个列表放入某个位置的堆中。诸如Haskell之类的完全不可变的语言会产生大量垃圾,而无需进行积极的优化和共享。具有只能用于<50个元素的集合不值得放在标准库中。

此外,不可变集合通常具有与其可变的集合完全不同的实现。考虑一下ArrayList,高效的不可变ArrayList根本不是数组!应该使用具有大分支因子的平衡树来实现,Clojure使用32 IIRC。通过添加功能更新使可变集合“不可变”,就像内存泄漏一样,是一个性能错误。

此外,在Java中共享不可行。Java为可变性和引用相等性提供了太多不受限制的钩子,以致于共享“仅仅是一种优化”。如果您可以修改列表中的元素,并且意识到您刚刚修改了该列表的其他20个版本中的元素,可能会有点烦您。

这还排除了非常重要的优化的大类,以实现有效的不可变性,共享,流融合(随便命名),可变性破坏了它。(这将是FP福音传教士的一个很好的口号)


21
我的示例谈到了不可变的接口。Java可以为这些接口提供一整套可变和不可变的实现,这将产生必要的权衡。程序员应酌情选择可变或不可变。程序员必须知道何时使用列表与集合。在出现性能问题之前,通常不需要可变版本,然后才可能需要作为构建器。无论如何,拥有不可变的接口本身就是一个胜利。
GlenPeterson

4
我再次阅读了您的答案,我认为您是在说Java具有可变性的基本假设(例如Java Bean),并且这些集合只是冰山一角,而消除该技巧将无法解决潜在的问题。一个有效点。我可能会接受此答案,并加快采用Scala的速度!:-)
GlenPeterson

8
我不确定不可变集合是否需要共享通用部分的功能才有用。Java中最常见的不可变类型,即字符的不可变集合,用于允许共享,但现在不再允许。使其有用的关键是能够将数据从a快速复制String到a StringBuffer,对其进行操作,然后再将数据复制到新的不可变String。将这样的模式与集合和列表一起使用可能会好于使用旨在促进产生稍微变化的实例的不可变类型,但仍然会更好……
supercat

3
完全有可能使用共享在Java中创建不可变的集合。集合中存储的项目是引用,并且其引用对象可能会发生突变-那么呢?这种行为已经破坏了现有的集合,例如HashMap和TreeSet,但是这些集合是用Java实现的。而且,如果多个集合包含对同一对象的引用,则完全可以预期修改该对象将导致从所有集合中查看该更改时可见。
所罗门诺夫的秘密

4
jozefg,完全有可能通过结构共享在JVM上实现有效的不可变集合。Scala和Clojure将它们作为标准库的一部分,这两种实现都基于Phil Bagwell的HAMT(哈希数组映射的Trie)。您关于Clojure使用BALANCED树实现不可变数据结构的陈述是完全错误的。
sesm 2015年

77

可变集合不是不可变集合的子类型。相反,可变且不可变的集合是可读集合的同级后代。不幸的是,“可读”,“只读”和“不可变”的概念似乎变得模糊起来,即使它们意味着三件事。

  • 可读的集合基类或接口类型保证可以读取项目,并且不提供任何直接的方法来修改集合,但不能保证接收引用的代码不能以允许修改的方式强制转换或操纵它。

  • 只读的集合接口不包含任何新成员,而只能由一个类实现,该类保证没有办法以某种方式操纵对它的引用以使集合变型或接收对某物的引用可以做到的。但是,它不保证该集合不会被引用内部的其他内容修改。请注意,只读收集接口可能无法阻止可变类的实现,但可以指定允许突变的任何实现或从实现派生的类都应被视为“非法”实现或实现的派生。

  • 不可变集合是只要存在对它的任何引用,它将始终保存相同数据的集合。不可变接口的任何实现方式都不能始终响应特定请求而返回相同的数据,这是不可行的。

有时具有实现或衍生自相同“可读”类型的高度关联的可变和不可变集合类型,并使可读类型包括,和方法AsImmutable,这很有用。这样的设计可以使想要将数据持久保存在集合中的代码调用; 如果该集合是可变的,则该方法将生成一个防御性副本,但如果该副本已经不可变,则跳过该副本。AsMutableAsNewMutableAsImmutable


1
好答案。不可变的集合可以为您提供与线程安全性有关的强大保证,以及随着时间的流逝如何推理它们的方法。可读/只读集合没有。实际上,为了遵守liskov替换原则,只读和不可变应该可能是具有最终方法和私有成员的抽象基类型,以确保没有派生类可以破坏该类型给定的garantee。或者它们应该是完全具体的类型,要么包装一个集合(只读),要么总是制作一个防御性副本(不可变)。这就是番石榴ImmutableList的工作方式。
Laurent Bourgault-Roy 2013年

1
@ LaurentBourgault-Roy:密封和可继承的不可变类型都有优点。如果不想让一个非法的派生类破坏其不变式,则密封类型可以提供保护,而可继承类则不提供任何保护。另一方面,与不了解任何类型的代码相比,可能知道一些关于其所保存数据的代码的存储空间紧凑得多。例如,考虑一个ReadableIndexedIntSequence类型,该类型封装了int,方法getLength()和的序列getItemAt(int)
2013年

1
@ LaurentBourgault-Roy:给定a ReadableIndexedIntSequence,可以通过将所有项目复制到数组中来生成数组支持的不可变类型的实例,但是假设特定实现只是返回了长度和((long)index*index)>>24每个项目的16777216 。那将是合法的不可变的整数序列,但是将其复制到数组将浪费大量时间和内存。
2013年

1
我完全同意。我的解决方案可以为您提供正确性(一定程度上),但是要获得大型数据集的性能,从一开始就必须具有持久的结构和设计以确保不变性。对于小型收藏,您可以不时获取不可变的副本。我记得Scala对各种程序进行了一些分析,发现所例举的清单中有90%的长度不超过10个。
Laurent Bourgault-Roy 2013年

1
@ LaurentBourgault-Roy:一个基本的问题是人们是否相信人们不会产生残缺的实现或派生类。如果是这样,并且如果接口/基类提供asMutable / asImmutable方法,则可以将性能提高许多数量级[例如,比较调用asImmutable上述序列实例的成本与构建的成本不可变数组支持的副本]。我认为为这种目的定义接口可能比尝试使用即席方法更好。恕我直言,最大的原因...
supercat 2013年

15

Java Collections Framework确实提供了通过java.util.Collections类中的六个静态方法来创建集合的只读版本的功能:

正如有人在对原始问题的评论中指出的那样,返回的集合可能不会被认为是不变的,因为即使无法修改集合(不能从此类集合中添加或删除成员),集合所引用的实际对象如果其对象类型允许,则可以对其进行修改。

但是,无论代码返回单个对象还是对象的不可修改集合,这个问题都将仍然存在。如果该类型允许其对象发生突变,则该决定是在该类型的设计中做出的,我看不到对JCF所做的更改如何改变它。如果不可变性很重要,则集合的成员应为不可变类型。


4
如果包装器包含指示被包装的东西是否已经不可变的指示,则存在不可修改的集合的设计将大大增强,并且还有其他方法immutableList。工厂方法将围绕传入的副本返回只读包装器list 除非传入的列表已经是不可变的。创建这样的用户定义类型将很容易,但是有一个问题:joesCollections.immutableList方法无法识别不需要复制由返回的对象fredsCollections.immutableList
超级猫2014年

8

这个问题问得好。我很喜欢这样一种想法:在用Java编写并在全球数百万台计算机上运行的所有代码中,每天,每天24小时不间断地浪费掉全部时钟周期的一半,而要做的只是制作副本的安全副本。由函数返回。(并在创建这些集合后数毫秒内对其进行垃圾回收。)

一定比例的Java程序员知道类unmodifiableCollection()的方法族的存在Collections,但是即使在其中,许多人也不用理会它。

而且我不能怪他们:一个假装为可读写但UnsupportedOperationException如果您错误地调用其“写”方法中的任何一个都会抛出一个接口是一件很邪恶的事情!

现在,像一个接口Collection,其将被错过add()remove()clear()方法不会是一个“ImmutableCollection”界面; 这将是一个“ UnmodifiableCollection”接口。实际上,永远不会有“ ImmutableCollection”接口,因为不变性是实现的本质,而不是接口的特征。我知道,这还不是很清楚。让我解释。

假设有人将这种只读收集界面交给您;将它传递给另一个线程安全吗?如果您确定它代表了一个真正不变的集合,那么答案将是“是”。不幸的是,由于它是一个接口,所以您不知道它是如何实现的,因此答案必须是“ 否”:就您所知,它可能是一个对您来说实际上是可变的集合的(对您而言)不可修改的视图, (就像您从中获得的东西一样Collections.unmodifiableCollection()),因此在另一个线程对其进行修改时尝试从中读取将导致读取损坏的数据。

因此,您实质上描述的是一组不是“不可修改”的集合,而是一组“不可修改的”集合接口。重要的是要理解,“不可修改的”仅意味着阻止引用此接口的任何人修改基础集合,并且之所以被阻止是因为接口缺乏任何修改方法,而不是因为基础集合不一定是不变的。底层集合很可能是可变的。您对此一无所知,也无法控制。

为了拥有不可变的集合,它们必须是,而不是接口!

这些不可变的集合类必须是最终的,因此,当您引用此类集合时,无论您是什么人,或任何引用它的人,都可以肯定地知道该集合将充当不可变的集合。用它做。

因此,为了在Java(或任何其他声明式命令性语言)中具有完整的集合集,我们需要以下内容:

  1. 一组不可修改的收集接口

  2. 一组可变的收集接口,扩展了不可修改的接口

  3. 一组实现可变接口的可变集合,并且扩展了不可修改的接口。

  4. 一组不可变的集合,实现了不可修改的接口,但大多数都作为类来传递,以确保不可变。

我已经实现了以上所有内容的乐趣,并且在项目中使用了它们,并且它们的工作就像是一种魅力。

它们不属于Java运行时的原因可能是因为人们认为这样做太多/太复杂/太难理解了。

就我个人而言,我认为上面所描述的还不够。似乎还需要做的另一件事是一组可变的接口和类,以实现结构不变性。(因为前缀“ StructurallyImmutable”太长了,所以可以简称为“刚性”。)


好点。两个细节:1.不可变集合需要特定的方法签名,特别是(以List为例):List<T> add(T t)-所有“ mutator”方法必须返回一个反映更改的新集合。2.不论好坏,接口通常除了签名之外还代表合同。可序列化就是这样一种接口。同样,Comparable要求您正确实现您的compareTo()方法以正确运行,并且理想情况下与equals()and 兼容hashCode()
GlenPeterson

哦,我什至都没有想到逐份复制的不变性。我在上面写的内容是指简单的,不可变的简单集合,实际上没有像这样的方法add()。但我想,如果mutator方法将被添加到不变类,那么他们将需要返回不变类。因此,如果潜伏在那里,我看不到它。
Mike Nakis

您的实施公开吗?我应该在几个月前问过。无论如何,我的是:github.com/GlenKPeterson/UncleJim
GlenPeterson,2015年

4
Suppose someone hands you such a read-only collection interface; is it safe to pass it to another thread?假设有人向您传递了可变集合接口的实例。调用任何方法是否安全?您不知道实现不会永远循环,不会引发异常或完全不考虑接口协定。为什么要针对不可变的收藏制定双重标准?
2013年

1
恕我直言,您对可变接口的推理是错误的。您可能编写了可变接口的可变实现,然后中断了。当然。但这是您的错,因为您违反合同。只是停止这样做。这SortedSet与通过使用不合格的实现将子集细分来打破a没有什么不同。或通过不一致Comparable。如果您愿意,几乎任何东西都可以损坏。我想这就是@Doval所说的“双重标准”。
maaartinus

2

与其他对象相比,不可变集合可以进行深度递归,并且如果对象相等是通过secureHash实现的,则不会造成不合理的低效率。这就是所谓的默克尔森林。它可以针对每个集合,也可以位于其中的一部分内,例如排序映射的(自平衡二进制)AVL树。

除非这些集合中的所有java对象都具有唯一的ID或要哈希的某些位串,否则该集合就没有哈希值可以唯一地命名自己。

示例:在我的4x1.6ghz笔记本电脑上,我可以每秒运行200K sha256s,其最小大小适合1个哈希周期(最多55个字节),而long哈希表中的500K HashMap ops或3M ops。对于某些数据完整性和匿名全局可伸缩性很重要的事情,每秒200K / log(collectionSize)新集合的速度足够快。


-3

性能。就其性质而言,馆藏可能很大。将1000个元素复制到具有1001个元素的新结构中,而不是插入单个元素,简直太可怕了。

并发。如果您有多个正在运行的线程,他们可能想要获取集合的当前版本,而不是获取12个小时前线程启动时传递的版本。

存储。在多线程环境中使用不可变的对象,您可以在其生命周期的不同点最终获得“相同”对象的数十个副本。对于Calendar或Date对象无关紧要,但是当它包含10,000个小部件时,它将杀死您。


12
不可变的集合只需要复制就可以了,因为您像Java那样普遍存在可变性,因此您不能共享。对于不可变的集合,并发通常更容易,因为它们不需要锁定。为了获得可见性,您始终可以对可变集合(在OCaml中常见)进行可变引用。通过共享,更新基本上是免费的。与可变结构相比,对数分配可能更多,但是在更新时,可以立即释放或重用许多已过期的子对象,因此不一定有更高的内存开销。
乔恩·普迪

4
夫妻问题。Clojure和Scala中的集合都是不可变的,但支持轻量级副本。添加元素1001意味着复制少于33个元素,并添加一些新的指针。如果在线程之间共享可变集合,则在更改它时会遇到各种同步问题。像“ remove()”这样的操作是噩梦般的。同样,不可变的集合可以可变地构建,然后一次复制到一个安全的版本中,可以在线程之间共享。
GlenPeterson

4
用并发作为反对不变性的理由很不寻常。重复。
汤姆·哈特芬

4
有点对这里的反对票表示不满。OP询问了为什么它们没有实现不可变的集合,并且我提供了对该问题的深思熟虑的答案。大概时尚意识中唯一可以接受的答案是“因为他们犯了一个错误”。我实际上有一些经验,纯粹是由于性能不佳而不得不使用原本出色的BigDecimal类来重构大量代码,这是因为不可变性是使用double加上乱七八糟来固定小数的512倍。
詹姆斯·安德森

3
@JamesAnderson:我的回答是:“性能”问题-您可以说现实生活中的不可变集合总是实现某种形式的共享和重用,从而避免了您所描述的问题。“并发性”-参数归结为“如果您想要可变性,那么不可变对象不起作用”。我的意思是,如果存在“同一事物的最新版本”的概念,那么某些事物需要发生改变,无论是事物本身还是拥有事物的事物。在“存储”中,您似乎说有时不需要可变性。
jhominal 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.