Java团队做了很多出色的工作,消除了Java 8中函数编程的障碍。特别是,对java.util Collections的更改在将转换链接到非常快速的流式操作方面做得很好。考虑到他们在集合上添加一流的函数和函数方法做得多么出色,为什么他们完全无法提供不可变的集合甚至不可变的集合接口?
不更改任何现有代码,Java团队可以随时添加与可变接口相同的不可变接口,减去setter方法,并使现有接口从其扩展,如下所示:给定键的值,指示操作是成功还是失败。不可变集合必须将此类方法视为工厂,并返回一个包含添加元素的新集合-该集合与当前签名不兼容。但这可以通过使用不同的方法名称(如List.add()Map.put()ImmutableList.append())来解决。产生的冗长性将远远超过使用不可变集合的好处所抵消,并且类型系统将防止调用错误方法的错误。随着时间的流逝,旧方法可能会过时。方法采用不可变的集合接口,您知道它不会修改该集合。如果某个方法返回一个不可变的集合,则说明您无法对其进行修改。
并发-可以在线程之间安全地共享不可变的集合。作为尝过假定不变性的语言的人,很难回到猖mutation的突变的狂野西部。 Clojure的集合(序列抽象)已经具有Java 8集合提供的所有功能以及不可变性(尽管由于同步的链表而不是流,因此可能会使用额外的内存和时间)。 Scala具有可变的集合和不可变的集合,具有完整的操作集,尽管这些操作很急切,但调用.addAt()可以提供一个惰性视图(还有其他惰性评估它们的方法)。我看不出没有不变的集合,Java将如何继续竞争。
有人可以指出我的历史或讨论吗?当然在某个地方是公开的。

评论

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

在您的层次结构中,我可以给您一个ImmutableList,然后在您不希望它更改时将其更改,因为这可能会破坏很多事情,因为只有const集合

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

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

嗯如果您的函数在该图中使用了ImmutableList,那么人们可以传递可变列表吗?不,这是对LSP的严重违反。

#1 楼

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

此外,不可变的集合通常具有与可变的集合不同的实现。考虑一下ArrayList,高效的不变ArrayList根本不是数组!应该使用具有大分支因子的平衡树来实现它,Clojure使用32 IIRC。就像添加内存泄漏一样,仅添加功能更新就使可变集合“不可变”是一种性能错误。 Java为可变性和引用相等性提供了太多不受限制的钩子,使得共享“仅仅是一种优化”。如果您可以修改列表中的元素,并且意识到您刚刚修改了列表中其他20个版本中的元素,可能会有点烦您。有效的不变性,共享,流融合的非常重要的优化,您将其命名为可变性。 (这对于FP宣传人员来说是一个很好的口号)

评论


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

– GlenPeterson
2013年12月18日17:21



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

– GlenPeterson
13年12月18日在17:28

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

–超级猫
2014年1月17日19:30

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

–恢复莫妮卡
15年8月13日在21:51

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

–sesm
2015年9月6日在20:54



#2 楼

可变集合不是不可变集合的子类型。相反,可变且不可变的集合是可读集合的同级后代。不幸的是,“可读”,“只读”和“不可变”的概念似乎变得模糊起来,即使它们表示三种不同的含义。或接口类型保证可以读取项目,并且不提供任何直接修改集合的方法,但不能保证接收引用的代码不能以允许修改的方式强制转换或操纵它。只读集合接口不包含任何新成员,而只能由一个类实现,该类保证没有办法以某种方式操纵对它的引用以使集合变型或接收对某些可以这样做。但是,它不保证该集合不会被引用内部的其他内容修改。请注意,只读收集接口可能无法阻止可变类的实现,但可以指定允许突变的任何实现或从实现派生的类均应视为“非法”实现或实现的派生。
不可变集合是只要存在对它的任何引用,它将始终保存相同数据的集合。不能始终响应特定请求返回相同数据的不可变接口的任何实现都被破坏。

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

评论


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

– Laurent Bourgault-Roy
2013年12月23日15:12



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

–超级猫
2013年12月23日在16:34



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

–超级猫
13年12月23日在16:39

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

– Laurent Bourgault-Roy
2013年12月23日在16:43



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

–超级猫
2013年12月23日的16:51

#3 楼

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


unmodifiableCollection(Collection c)
unmodifiableList(列表列表)
unmodifiableMap(地图m)
unmodifiableSet(集合s)
unmodifiableSortedMap(SortedMap m)
unmodifiableSortedSet(SortedSet s) br />正如有人在对原始问题的评论中指出的那样,返回的集合可能不会被视为不可变的,因为即使无法修改集合(不能从此类集合中添加或删除成员),实际引用的对象如果集合的对象类型允许,则可以修改此集合。如果该类型允许其对象发生突变,则该决定是在该类型的设计中做出的,我看不到对JCF的更改如何改变它。如果不可变性很重要,则集合的成员应为不可变类型。

评论


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

–超级猫
14年7月14日在16:27

#4 楼

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

有一定比例的Java程序员意识到unmodifiableCollection()类的Collections方法家族的存在,但是即使在其中,许多人仍然不用理它。

我不能怪他们:一个假装为可读写但如果您错误地调用其“写”方法的接口将抛出UnsupportedOperationException的接口是很邪恶的。

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

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

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

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

这些不可变的集合类必须是最终的,这样,当给您引用此类集合时,您肯定会知道无论您是什么人,或其他任何人,它都将作为不可变的集合运行

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


一组不可修改的集合接口。
一组可变的集合接口,扩展了不可修改的集合。
一组实现可变接口的可变集合类,以及扩展地说,也就是不可修改的接口。
一组不可修改的集合类,实现了不可修改的接口,但是大多数情况下作为类传递,以保证不可变。

我已经实现了所有上面的内容很有趣,我正在项目中使用它们,它们像魅力一样工作。

它们之所以如此不是Java运行时的一部分,可能是因为人们认为这样做太多/太复杂/太难理解了。

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

评论


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

– GlenPeterson
15年4月4日在16:48

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

–迈克·纳基斯(Mike Nakis)
2015年6月4日17:16

假设有人将这种只读的收集界面交给您;将它传递给另一个线程是否安全?假设有人向您传递了可变集合接口的实例。调用任何方法是否安全?您不知道实现不会永远循环,抛出异常或完全不考虑接口协定。为什么要为不可变的收藏制定双重标准?

–Doval
16年6月22日在14:59

恕我直言,您对可变接口的推理是错误的。您可能编写了可变接口的可变实现,然后中断了。当然。但这是您的错,因为您违反合同。只是停止这样做。这与通过使用不合格的实现对子集进行分类来破坏SortedSet没什么不同。或通过传递不一致的可比性。如果需要,几乎任何东西都可以破坏。我猜这就是@Doval所说的“双重标准”。

– maaartinus
17年8月23日在23:12

@maaartinus问题是,即使在不变性的上下文之外,不可修改的(只读)接口也很有用。类可以合法地公开其包含的可变集合的可简化界面,并可以通过其他方式对集合进行变异。因此,不可修改的收集接口不可能强加一个合同,该合同说支持集合必须是不可变的,因为在某些合法用法中,支持集合不是不可改变的。

– Mike Nakis
17年8月24日在6:34

#5 楼

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

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

#6 楼

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

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

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

评论


不可变的集合仅因您像Java一样具有广泛的可变性而无法共享时才需要复制。不可变集合通常不需要并发锁定,因此并发通常更容易。为了获得可见性,您始终可以对可变集合(在OCaml中常见)进行可变引用。通过共享,更新基本上是免费的。与可变结构相比,您可以做对数分配,但是在更新时,许多已过期的子对象可以立即释放或重用,因此不一定有更高的内存开销。

–琼·普迪(Jon Purdy)
2013年12月19日上午1:32

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

– GlenPeterson
2013年12月19日下午2:33

用并发作为反对不变性的论点是不寻常的。重复。

– Tom Hawtin-大头钉
2013年12月20日在1:41



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

–詹姆斯·安德森(James Anderson)
13 Dec 20'3:33



@JamesAnderson:我的回答是:“性能”问题-您可以说现实生活中的不可变集合总是实现某种形式的共享和重用,从而避免了您所描述的问题。 “并发”-参数归结为“如果您想要可变性,那么不可变对象不起作用”。我的意思是,如果存在“同一事物的最新版本”的概念,那么某些事物需要发生变化,无论是事物本身还是拥有事物的事物。在“存储”中,您似乎说有时不需要可变性。

– jhominal
2013年12月23日15:42