Java 中不可变对象的优势似乎很明显:
- 一致状态
- 自动线程安全
- 简单
您可以通过使用私有最终字段和构造函数注入来支持不变性。
但是,在 Java 中偏爱不可变对象有什么缺点呢?
IE
- 与 ORM 或 Web 演示工具不兼容?
- 不灵活的设计?
- 实施复杂性?
是否可以设计一个主要使用不可变对象的大规模系统(深度对象图)?
Java 中不可变对象的优势似乎很明显:
您可以通过使用私有最终字段和构造函数注入来支持不变性。
但是,在 Java 中偏爱不可变对象有什么缺点呢?
IE
是否可以设计一个主要使用不可变对象的大规模系统(深度对象图)?
但是,在 Java 中偏爱不可变对象有什么缺点呢?与 ORM 或 Web 演示工具不兼容?
基于反射的框架因不可变对象而变得复杂,因为它们需要构造函数注入:
实施复杂性?
创建不可变对象仍然是一项无聊的任务;编译器应该注意实现细节,就像在groovy中一样
是否可以设计一个主要使用不可变对象的大规模系统(深度对象图)?
绝对是的;不可变对象为其他对象提供了很好的构建块(它们有利于组合),因为当您可以依赖其不可变组件时,维护复杂对象的不变量会容易得多。对我来说唯一真正的缺点是创建许多临时对象(例如String concat 在过去是一个问题)。
有了不变性,任何时候你需要修改数据,你都需要创建一个新对象。这可能很昂贵。
想象一下,需要修改消耗几兆内存的对象中的一位:您需要实例化一个全新的对象、分配内存等。如果您需要多次这样做,可变性就变得非常有吸引力。
如果你追求可变性,那么你会发现,每当你需要调用一个你不想让对象改变的方法,或者你需要返回一个属于内部状态的对象时,你都需要进行防御复制。
如果您真的查看使用可变对象的程序,您会发现它们很容易通过修改来“攻击”:
这个问题不会经常出现,因为大多数程序不会更改数据(它们实际上是不可变的,因为它们永远不会改变)。
我个人将我可能做的每一件事都做好了。我可能将 90%-95% 的所有变量(参数、本地、实例、静态、异常等)标记为最终变量。在某些情况下它必须是可变的,但绝大多数情况下它不是。
我认为这可能取决于你的注意力。如果您正在为第三方编写库以供使用,那么您会比如果您正在编写只有您(或您的团队)将维护的应用程序考虑得更多。
我发现你可以为系统的大部分使用不可变对象编写大型应用程序,而不会带来太多痛苦。
从根本上说,在现实世界中,与许多特定身份相关的状态会发生变化。如果我问“乔的别克现在的位置”是什么,今天它可能是西雅图的一个位置,明天它可能是洛斯阿拉莫斯的一个位置。可以定义和创建一个GeographicLocation
对象,其值将始终表示 Joe's Buick 在某个特定时刻所处的位置,并且永远不会改变——如果今天它表示西雅图的一个地点,那么它将一直如此。然而,这样的物体将没有作为“乔的别克的当前位置”的持续身份。
也可以定义事物,以便有一个VehicleLocation
连接到 Joe's Buick 的对象,使得该对象始终表示“Joe's Buick 的当前位置”。即使汽车四处移动,这样的对象也可以保留其作为“乔的别克车的当前位置”的身份,但不会代表一个恒定的地理位置。如果考虑到 Joe 将他的别克车卖给 Bob 并购买了一辆福特汽车的场景,那么定义“身份”可能会很棘手——对象应该跟踪“乔的福特汽车的当前位置”还是“鲍勃的别克汽车的当前位置”——但在在许多情况下,可以通过使用保证对象身份的某些方面永远不会改变的数据模型来避免此类问题。
一个对象的所有内容都不可能是不可变的。如果一个对象是不可变的,那么它就不能有一个不可变的身份来封装超出其当前状态的任何内容。然而,如果一个对象是可变的,它可以具有一个不可变的身份,其意义超越了它的当前状态。在许多情况下,拥有不可变身份比拥有不可变状态更有用,在这种情况下,可变对象几乎是必不可少的。虽然在某些情况下可以通过拥有一个不可变对象来“模拟”可变对象,该对象将搜索不可变对象的最新版本以查找可能在一个版本和下一个版本之间“更改”的信息,但这种方法通常是效率极低。
你几乎回答了你自己的问题。我不相信 JavaBean 规范提到了关于不变性的任何内容,但 JavaBean 是许多 Java 框架的基础。
对于习惯于命令式编程风格的人来说,不可变类型的概念有些不常见。但是,在许多情况下,不变性具有很大的优势,您已经命名了最重要的那些。
有很好的方法来实现不可变的平衡树、队列、堆栈、出队和其他数据结构。事实上,许多现代编程语言/框架仅支持不可变字符串,因为它们具有优势,有时还支持其他对象。
对于不可变对象,如果需要更改值,则必须将其替换为新实例。根据对象的生命周期,用不同的实例替换它可能会增加永久(长期)垃圾收集时间。如果对象在内存中保留足够长的时间以放置在终身代中,这将变得更加关键。
Java 中的问题是必须处理所有这些对象,其中类看起来像:
class Mutable {
State1 f1;
MoreState f2;
void doSomething() { // mutate the state, but don't document it }
void doSomethingElse() /// mutate the state heavily, do not mention in doc
}
(注意缺少的 Cloneable 接口)。
如今,垃圾收集器的问题已经不是那么大了。虚拟机对短命的对象很满意。
编译器/JIT 技术的进步迟早将有可能优化中间临时对象的创建。例如:
BigInteger three =, two =, i1 = ...;
BigInteger i2 = i1.mul(three).div(two);
JIT 可能会注意到中间对象 i1.mul(three) 可用于最终结果,并调用可用于可变累加器的 div 方法的变体。
请参阅函数式 Java以获得对您问题的全面回答。
与其他所有设计模式一样,不可变性只应在需要时使用。你举了线程安全的例子:在一个高度线程化的应用程序中,你可以更喜欢不变性而不是自己让它成为线程安全的额外费用。但是,如果您的设计要求对象是可变的,请不要仅仅因为“这是一种设计模式”而使它们不可变。
至于你的图,你可以选择让你的节点不可变,让另一个类来处理它们之间的连接,或者你可以创建一个可变节点来处理它自己的子节点并具有一个不可变的值类。
在 Java 中使用不可变对象的最大成本可能是未来的开发人员不会期待它或习惯于这种风格。期望大量记录或观察大量对象随着时间的推移产生可变对等点。
话虽如此,我能想到的避免不可变对象的唯一真正技术原因是 GC 流失。对于大多数应用程序,我不认为这是避免它们的令人信服的理由。
我用大约 90% 的不可变对象做过的最重要的事情是一个玩具式的解释器,所以它当然可以完成复杂的 Java 项目。
在不可变数据中,您不会设置两次...请参阅 haskell 和 scala vals(以及 clojure of cource)...
例如..对于数据结构..像树一样,当您对树执行写操作时,实际上您是在不可变树之外添加元素..完成后..树和分支重新组合在一个新树..所以像这样你可以非常安全地执行并发读写..
在传统模型中,您必须锁定一个值,因为它可以随时重置..所以..您最终会遇到线程非常热的区域..因为它们无论如何都会按顺序执行..
使用不可变数据,您不会多次设置..它是一种全新的编程方式..您最终可能会使用更多的内存..但是并行化是自然而无痛的..
与任何工具一样,您必须知道何时使用它,何时不使用它。
就像 Tehblanx 指出的那样,如果你想改变一个持有不可变对象的变量的状态,你必须创建一个新对象,这可能会很昂贵,尤其是当对象又大又复杂的时候。绝对正确,但这仅仅意味着您必须明智地决定哪些对象应该是可变的,哪些应该是不可变的。如果有人说所有对象都应该是不可变的,那简直就是胡说八道。
我倾向于说表示单个逻辑“事实”的对象应该是不可变的,而表示多个事实的对象应该是可变的。比如,整数或字符串应该是不可变的。包含姓名、地址、当前金额、上次购买日期等的“客户”对象应该是可变的。当然,我可以立即想到这样一条一般规则的一百个例外。我一直在做的一个例外是,当我有一个作为包装器存在的类时,在某些情况下原语是不合法的(例如在集合中)保存原语,但我需要不断更新它。
在 Java 中,一个方法不能返回多个对象,例如return a, b, c
. 返回一个对象数组会使代码看起来很难看。在这种情况下,我必须将可变对象传递给方法并让它改变这些对象的状态。但是,我不知道返回多个对象是否是代码异味。
答案是否定的。没有任何好的理由是可变的。
您确实遇到了许多需要可变对象才能使用它们的框架(或框架版本)的问题(Spring 我正朝着您的方向发展)。当您与他们一起工作并浏览代码时,您会愤怒地握紧拳头,因为您需要将肮脏的可变性引入本来可以很容易避免的辉煌代码块中。
我确信存在有限的极端情况(可能比任何事情都更假设),其中对象创建和收集的开销是不可接受的。但我敦促提出这个论点的人看看像 scala 这样的语言,其中包含的集合在默认情况下是不可变的,然后看看基于该概念构建的大量性能关键应用程序。
这当然是夸张。实际上,您应该首先考虑不变性,看看它是否会给您带来任何可衡量的问题,如果确实会引入可变性,但请确保您可以证明它可以解决您的问题。否则,您只是无益地承担了责任。在这样做时,我认为您会发现很难提出“实施复杂性”和“不灵活”的客观案例。
不可变对象的一些实现具有更新不可变对象的事务性方法。类似于数据库如何提供安全的提交和回滚。但与这里的许多答案形成鲜明对比。不可变对象永远不会改变。一个典型的操作是。
B = append(A,C)
B 是一个新对象。就像 A 和 C 一样。没有对 A 或 C 进行任何修改。在内部,红黑树实现使这种语义足够快,可以使用。
缺点是它不如使操作到位那么快。但这只是比较系统的一个部分。在评估可能的缺点时,我们需要将系统视为一个整体。而且我个人对整个影响并不清楚。尽管我怀疑不变性最终会胜出。
I know some experts contend there is contention at the top level of the red black tree. And that has a negative effect in throught-put.
我对不可变数据结构的最大担忧是如何保存/重构它们。也就是说,如果一个类有最终字段,我不能实例化它然后设置它的字段。