如果不变的对象¹很好,简单并且可以在并发编程中受益,那么为什么程序员会继续创建可变的对象²?
我有4年的Java编程经验,正如我所看到的,创建类之后,人们要做的第一件事就是在IDE中生成getter和setter(因此使其可变)。是否缺乏意识,或者在大多数情况下我们能否摆脱使用可变对象的困扰?
如果不变的对象¹很好,简单并且可以在并发编程中受益,那么为什么程序员会继续创建可变的对象²?
我有4年的Java编程经验,正如我所看到的,创建类之后,人们要做的第一件事就是在IDE中生成getter和setter(因此使其可变)。是否缺乏意识,或者在大多数情况下我们能否摆脱使用可变对象的困扰?
Answers:
可变对象和不可变对象都有各自的用途,优点和缺点。
在许多情况下,不可变的对象确实确实使生活变得更简单。它们特别适用于值类型,其中对象没有标识,因此可以轻松替换它们。而且,它们可以使并发编程方式更安全,更干净(众所周知,并发错误中的大多数很难最终发现是由线程之间共享的可变状态引起的)。但是,对于大型和/或复杂的对象,为每个单个更改创建对象的新副本可能会非常昂贵和/或乏味。对于具有独特标识的对象,更改现有对象比创建新的,修改后的副本要简单和直观得多。
考虑一个游戏角色。在游戏中,速度是重中之重,因此与可变对象代表游戏角色相比,替代行为可能会为每一个小小的变化生成新的游戏角色副本,从而使游戏的运行速度大大提高。
此外,我们对现实世界的感知不可避免地基于可变对象。当您在加油站为汽车加油时,您始终将其视为相同的对象(即,其身份在状态改变时得以保持)-好像旧的空罐车被连续的新车替换一样坦克越来越满的汽车实例。因此,每当我们在程序中对某些实际领域建模时,使用可变对象来代表现实世界实体来实现领域模型通常会更直接,更容易。
除了所有这些合理的理由外,a,人们继续创造可变物体的最可能原因是心智惯性,也就是对变化的抵制。请注意,当今的大多数开发人员在不变性(以及包含的范例,函数式编程)在他们的影响范围内变得“时髦”之前就已经接受了良好的培训,并且不让他们对我们的交易的新工具和方法保持最新的知识-实际上,我们许多人都积极抵制新观念和新进程。“我已经这样编程了nn年了,我不在乎最新的愚蠢时尚!”
Point
例如,.NET中的类是不可变的,但是由于更改而创建新的点很容易,因此可以承受。通过将“运动部件”去耦,可以使制作不可变角色的动画变得非常便宜(但是,是的,某些方面是可变的)。(2)“大型和/或复杂的对象”很可能是不可变的。字符串通常很大,通常会受益于不变性。我曾经将复杂的图形类重写为不可变的,从而使代码更简单,更高效。在这种情况下,具有可变的构建器是关键。
我想你们都错过了最明显的答案。大多数开发人员创建可变对象,因为可变性是命令式语言的默认设置。与不断修改远离默认值的代码(不管是否正确)相比,我们大多数人与我们的时间关系更好。不变性不是任何其他方法的灵丹妙药。正如某些答案所指出的,这使某些事情变得容易,但使其他事情变得更加困难。
final
,const
等需要一点额外的努力......除非你设置了代码模板:-)
有一个可变性的地方。 领域驱动的设计原则对什么应该是可变的和什么是不可变的提供了扎实的理解。如果您考虑一下,您将意识到构想一个系统,在该系统中,对对象的每次状态更改都需要销毁该对象以及对引用它的每个对象进行重新组合。对于复杂的系统,这很容易导致完全擦除和重建整个系统的对象图
大多数开发人员不会在性能要求足够重要以至于需要专注于并发性(或许多其他问题,而知情人士普遍认为是好的做法)方面做不到的事情。
有些事情是您无法使用不可变对象完成的,例如具有双向关系。一旦在一个对象上设置了关联值,它的身份就会改变。因此,您在另一个对象上设置了新值,并且它也发生了变化。问题在于第一个对象的引用不再有效,因为已经创建了一个新实例来用引用表示该对象。继续这样做只会导致无限回归。阅读您的问题后,我做了一些案例研究,这是什么样子。您是否有另一种方法可以在保持不变性的同时允许此类功能?
public class ImmutablePerson {
public ImmutablePerson(string name, ImmutableEventList eventsToAttend)
{
this.name = name;
this.eventsToAttend = eventsToAttend;
}
private string name;
private ImmutableEventList eventsToAttend;
public string Name { get { return this.name; } }
public ImmutablePerson RSVP(ImmutableEvent immutableEvent){
// the person is RSVPing an event, thus mutating the state
// of the eventsToAttend. so we need a new person with a reference
// to the new Event
ImmutableEvent newEvent = immutableEvent.OnRSVPReceived(this);
ImmutableEventList newEvents = this.eventsToAttend.Add(newEvent));
var newSelf = new ImmutablePerson(name, newEvents);
return newSelf;
}
}
public class ImmutableEvent {
public ImmutableEvent(DateTime when, ImmutablePersonList peopleAttending, ImmutablePersonList peopleNotAttending){
this.when = when;
this.peopleAttending = peopleAttending;
this.peopleNotAttending = peopleNotAttending;
}
private DateTime when;
private ImmutablePersonList peopleAttending;
private ImmutablePersonList peopleNotAttending;
public ImmutableEvent OnReschedule(DateTime when){
return new ImmutableEvent(when,peopleAttending,peopleNotAttending);
}
// notice that this will be an infinite loop, because everytime one counterpart
// of the bidirectional relationship is added, its containing object changes
// meaning it must re construct a different version of itself to
// represent the mutated state, the other one must update its
// reference thereby obsoleting the reference of the first object to it, and
// necessitating recursion
public ImmutableEvent OnRSVPReceived(ImmutablePerson immutablePerson){
if(this.peopleAttending.Contains(immutablePerson)) return this;
ImmutablePersonList attending = this.peopleAttending.Add(immutablePerson);
ImmutablePersonList notAttending = this.peopleNotAttending.Contains( immutablePerson )
? peopleNotAttending.Remove(immutablePerson)
: peopleNotAttending;
return new ImmutableEvent(when, attending, notAttending);
}
}
public class ImmutablePersonList
{
private ImmutablePerson[] immutablePeople;
public ImmutablePersonList(ImmutablePerson[] immutablePeople){
this.immutablePeople = immutablePeople;
}
public ImmutablePersonList Add(ImmutablePerson newPerson){
if(this.Contains(newPerson)) return this;
ImmutablePerson[] newPeople = new ImmutablePerson[immutablePeople.Length];
for(var i=0;i<immutablePeople.Length;i++)
newPeople[i] = this.immutablePeople[i];
newPeople[immutablePeople.Length] = newPerson;
}
public ImmutablePersonList Remove(ImmutablePerson newPerson){
if(immutablePeople.IndexOf(newPerson) != -1)
ImmutablePerson[] newPeople = new ImmutablePerson[immutablePeople.Length-2];
bool hasPassedRemoval = false;
for(var i=0;i<immutablePeople.Length;i++)
{
hasPassedRemoval = hasPassedRemoval || immutablePeople[i] == newPerson;
newPeople[i] = this.immutablePeople[hasPassedRemoval ? i + 1 : i];
}
return new ImmutablePersonList(newPeople);
}
public bool Contains(ImmutablePerson immutablePerson){
return this.immutablePeople.IndexOf(immutablePerson) != -1;
}
}
public class ImmutableEventList
{
private ImmutableEvent[] immutableEvents;
public ImmutableEventList(ImmutableEvent[] immutableEvents){
this.immutableEvents = immutableEvents;
}
public ImmutableEventList Add(ImmutableEvent newEvent){
if(this.Contains(newEvent)) return this;
ImmutableEvent[] newEvents= new ImmutableEvent[immutableEvents.Length];
for(var i=0;i<immutableEvents.Length;i++)
newEvents[i] = this.immutableEvents[i];
newEvents[immutableEvents.Length] = newEvent;
}
public ImmutableEventList Remove(ImmutableEvent newEvent){
if(immutableEvents.IndexOf(newEvent) != -1)
ImmutableEvent[] newEvents = new ImmutableEvent[immutableEvents.Length-2];
bool hasPassedRemoval = false;
for(var i=0;i<immutablePeople.Length;i++)
{
hasPassedRemoval = hasPassedRemoval || immutableEvents[i] == newEvent;
newEvents[i] = this.immutableEvents[hasPassedRemoval ? i + 1 : i];
}
return new ImmutableEventList(newPeople);
}
public bool Contains(ImmutableEvent immutableEvent){
return this.immutableEvent.IndexOf(immutableEvent) != -1;
}
}
Relationship(a, b)
;在创建关系的角度来看,这两个实体a
与b
已经存在,而且关系本身也是不可改变的。我并不是说这种方法在Java中是可行的。只是有可能。
R
is Relationship(a,b)
和the a
和b
are都是不可变的,则a
也b
不会保留对的引用R
。为了使此工作正常,必须将引用存储在其他位置(例如静态类)。我是否正确理解您的意图?
我一直在阅读“纯功能数据结构”,这使我意识到,有很多数据结构更容易使用可变对象实现。
为了实现二叉搜索树,您每次都必须返回一棵新树:您的新树将必须为每个已修改的节点制作一个副本(共享未修改的分支)。对于您的插入功能来说还算不错,但是对我来说,当我开始进行删除和重新平衡时,事情很快就变得效率低下。
要意识到的另一件事是,如果您的代码没有以暴露并发性问题的方式运行,那么您可能需要花费多年的时间编写面向对象的代码,而从未真正意识到共享可变状态的可怕性。
在我看来,这是缺乏认识的。如果您查看其他已知的JVM语言(Scala,Clojure),则在代码中很少看到可变对象,这就是为什么人们在单线程不够的情况下开始使用它们的原因。
我目前正在学习Clojure,并且在Scala方面有一点经验(也有4年以上Java经验),并且由于对状态的了解,您的编码风格也发生了变化。
我认为一个主要的促成因素已被忽略:Java Beans严重依赖于一种特定的突变对象样式,并且(尤其是考虑到源代码)很多人似乎将其视为所有 Java的典范示例(甚至是)。应该写。
我在职业生涯中使用过的每个企业Java系统都使用Hibernate或Java Persistence API(JPA)。Hibernate和JPA本质上要求您的系统使用可变对象,因为它们的全部前提是它们检测并保存对数据对象的更改。对于许多项目而言,Hibernate带来的易于开发性比不可变对象的优势更具吸引力。
显然,可变对象比Hibernate出现的时间长得多,因此Hibernate可能不是可变对象流行的最初“原因”。也许可变对象的流行使Hibernate蓬勃发展。
但是今天,如果许多初级程序员使用Hibernate或另一个ORM在企业系统上扎根,那么大概他们会养成使用可变对象的习惯。像Hibernate这样的框架可能正在巩固可变对象的流行。
尚未提及的主要要点是,使对象的状态可变是可以使封装该状态的对象的身份不可变的。
许多程序旨在对固有可变的现实事物进行建模。假设在12:51 am,有一个变量AllTrucks
持有对对象#451的引用,该对象是数据结构的根,该数据结构指示当时(12:51 am)车队的所有卡车中都装有什么货物,还有一个变量BobsTruck
可以用来获取对#24601对象的引用,该对象指向一个对象,该对象指示当时(12:51 am)Bob的卡车中所装的货物。在12:52 am,一些卡车(包括Bob的卡车)被装卸,并且数据结构被更新,因此AllTrucks
现在将保留对数据结构的引用,该数据结构指示到12:52 am为止所有卡车中的货物。
应该BobsTruck
怎么办?
如果每个卡车对象的'cargo'属性是不可变的,则对象#24601将永远代表鲍勃卡车在上午12:51处的状态。如果BobsTruck
直接引用对象#24601,则除非更新的代码AllTrucks
也同时发生更新BobsTruck
,否则它将不再代表鲍勃卡车的当前状态。还要注意的BobsTruck
是,除非以某种形式的可变对象存储,否则更新的代码AllTrucks
可以更新的唯一方法是对代码进行显式编程。
如果希望BobsTruck
在保持所有对象不变的情况下使用观察鲍勃卡车的状态,则可能具有BobsTruck
不变的功能,给定AllTrucks
在任何特定时间具有或具有的值,该函数将产生鲍勃卡车的状态为那时。一个甚至可能拥有一对不可变的功能-其中一个将如上,而另一个将接受对车队状态和新卡车状态的引用,并返回对新车队状态的引用匹配旧的,但鲍勃的卡车将恢复新状态。
不幸的是,每次有人想要访问鲍勃卡车的状态时都必须使用这种功能,这会变得很烦人且麻烦。一种替代方法是说对象#24601将永远(只要有人持有对它的引用)就代表鲍勃卡车的当前状态。想要重复访问Bob卡车当前状态的代码不必每次都运行一些耗时的功能-它只需执行一次查找功能即可找出对象#24601是Bob的卡车,然后简单地每当它想查看鲍勃卡车的当前状态时,都可以访问该对象。
请注意,在单线程环境或多线程环境中,函数方法并非没有优势,在多线程环境中,线程通常只会观察数据而不是更改数据。任何复制包含在其中的对象引用的观察者线程AllTrucks
然后检查代表的卡车状态,从其获取参考的那一刻起,将看到所有卡车的状态。每当观察者线程想要查看更新的数据时,它都可以重新获取引用。另一方面,用单个不变对象表示车队的整个状态将排除两个线程同时更新不同卡车的可能性,因为每个线程如果留给自己的设备将产生一个新的“车队状态”对象,其中包括卡车的新状态和其他旧状态。如果每个线程仅在未更改并响应失败时才使用CompareExchange
更新,则可以确保正确性AllTrucks
CompareExchange
通过重新生成其状态对象并重试该操作,但是如果有多个线程尝试同时进行写操作,则性能通常会比所有写操作都在单个线程上进行时要差。尝试进行此类同时操作的线程越多,性能将越差。
如果单个卡车对象是可变的但具有不变的标识,则多线程方案将变得更干净。在任何给定的卡车上一次只能允许一个线程运行,但是在不同卡车上运行的线程可以这样做而不会受到干扰。尽管即使使用不可变的对象,也有很多方法可以模仿这种行为(例如,可以定义“ AllTrucks”对象,以便将属于XXX的卡车的状态设置为SSS,仅需要生成一个表示“截至[时间],现在,属于[XXX]的卡车的状态为[SSS];其他所有状态为[AllTrucks的旧值]。生成此类对象的速度将足够快,即使在存在争用的情况下,CompareExchange
循环不会花很长时间。另一方面,使用这样的数据结构将大大增加找到特定人员的卡车所需的时间。使用具有不变身份的可变对象可以避免该问题。
没有对与错,这取决于您的喜好。有一个原因为什么有些人偏爱使用一种范例而不是另一种范例,以及使用一种数据模型而不是另一种范例的语言。这仅取决于您的喜好以及要实现的目标(并且能够轻松使用两种方法而不会疏远一方或另一方的顽固支持者是某些语言所追求的圣杯)。
我认为回答您问题的最佳和最快方法是让您掌握“不变性与可变性”的优缺点。
检查此博客文章:http : //www.yegor256.com/2014/06/09/objects-should-be-immutable.html。总结了为什么不变的对象比可变的对象更好。这是参数的简短列表:
据我了解,人们正在使用可变对象,因为他们仍在将OOP与命令式程序编程混合在一起。
在Java中,不可变的对象需要一个构造函数,该构造函数将采用该对象的所有属性(或者该构造函数从其他参数或默认值创建它们)。这些属性应标记为final。
与此相关的四个问题主要与数据绑定有关:
您可以使用类似的注释来减轻#1和#2的负担,@ConstructorProperties
并创建另一个可变的生成器对象(通常是流畅的)来创建不可变对象。
没有可变对象,您就没有状态。诚然,如果您可以管理它,并且有可能从多个线程中引用一个对象,那么这是一件好事。但是该程序将相当无聊。许多软件,尤其是Web服务器,通过在数据库,操作系统,系统库等上推销可变性来避免对可变对象负责。实际上,这确实使程序员摆脱了可变性问题并使Web(及其他)成为可能发展负担得起。但是可变性仍然存在。
通常,您有三种类型的类:普通的,非线程安全的类,必须仔细地加以保护。不变的类,可以自由使用;以及可以自由使用的可变线程安全类,但必须格外小心。第一种是麻烦的,最坏的是被认为是第三种的。当然,第一种类型很容易编写。
我通常会遇到很多普通的,易变的类,我必须非常仔细地观察它们。在多线程情况下,即使我可以避免致命的拥抱,同步也必须减慢一切。因此,我通常在制作可变类的不变副本,并将其交给任何可以使用它的人。每次原始变化时都需要一个新的不可变副本,因此我想有时我可能会拥有一百个原始副本。我完全依赖垃圾回收。
总之,如果您不使用多个线程,则非线程安全的可变对象会很好。(但是到处都是多线程,请多加注意!)如果可以将它们限制为局部变量或严格同步它们,则可以安全地使用它们。如果可以通过使用他人的经过验证的代码(DB,系统调用等)来避免使用它们,则可以这样做。如果可以使用不可变的类,请这样做。而且我认为,总的来说,人们要么没有意识到多线程问题,要么(明智地)对它们感到恐惧,并使用各种技巧来避免多线程(或者将责任推到其他地方)。
作为PS,我感觉到Java获取器和设置器已失去控制。检查这个出来。
很多人都给出了很好的答案,所以我想指出您所提及的内容,这些内容非常引人注目,而且非常真实,在这里没有其他地方提及。
自动创建setter和getters是一个可怕的想法,但这是具有过程意识的人们尝试将OO引入其思维方式的第一种方式。设置器和获取器以及属性仅应在发现需要时创建,而不是默认情况下创建
实际上,尽管您非常需要定期使用getter,但是setter或可写属性应该在代码中永远存在的唯一方式是通过构建器模式,在完全实例化对象之后将其锁定。
许多类在创建后都是可变的,这很好,只是不应该直接对其属性进行操作-而是应该要求它们通过其中带有实际业务逻辑的方法调用来操作其属性(是的,setter几乎相同直接操纵财产的事情)
现在,这也不是真正适用于“脚本”样式的代码/语言,而是适用于您为其他人创建的代码,并期望其他人多年来可以反复阅读。我最近不得不开始做出这种区分,因为我非常喜欢Groovy,并且目标也有很大的不同。
实例化对象后,如果必须设置倍数,则使用可变对象。
您不应有一个带有六个参数的构造函数。而是使用setter方法修改对象。
一个示例是一个Report对象,带有用于字体,方向等的设置器。
简而言之:当您有很多状态要设置给对象,并且很长的构造函数签名不可行时,可变变量会很有用。
编辑:生成器模式可用于生成对象的整个状态。
withFont
返回似乎也不好Report
。
我认为使用可变对象源于命令式思考:您通过逐步更改可变变量的内容(副作用计算)来计算结果。
如果您从功能上考虑,则希望具有不变的状态,并通过应用功能并从旧功能创建新值来表示系统的后续状态。
功能方法可以更简洁,更健壮,但是由于复制,它的效率可能非常低,因此您需要使用逐步修改的共享数据结构。
我认为最合理的权衡是:从不可变对象开始,如果实现速度不够快,则切换为可变对象。从这个角度来看,从一开始就系统地使用可变对象可以被认为是某种过早的优化:您从一开始就选择了更高效(但也更难以理解和调试)的实现。
那么,为什么许多程序员使用可变对象?恕我直言,有两个原因:
我知道您在问Java,但我在Objective-C中始终使用可变与不可变。有一个不可变数组NSArray和一个可变数组NSMutableArray。这是两个不同的类,它们以优化的方式专门编写以处理确切的用法。如果我需要创建一个数组并且从不更改其内容,则可以使用NSArray,它是一个较小的对象,与可变数组相比,它的执行速度要快得多。
因此,如果创建一个不可变的Person对象,则只需要一个构造函数和getters,因此该对象将更小并且使用更少的内存,从而使您的程序实际上更快。如果您需要在创建后更改对象,那么可变的Person对象会更好,以便它可以更改值而不是创建新对象。
因此:根据性能的不同,选择可变对象还是不可变对象可能会产生很大的不同。
除了这里给出的许多其他原因之外,一个问题是主流语言不能很好地支持不变性。至少,您因不变性而受到惩罚,因为您必须添加其他关键字(例如const或final),并且必须使用具有许多参数或代码冗长的构建器模式的难以理解的构造函数。
在考虑到不变性的语言中,这要容易得多。考虑以下Scala代码片段,以定义一个类Person(可以选择使用命名参数)并创建具有更改的属性的副本:
case class Person(id: String, firstName: String, lastName: String)
val joe = Person("123", "Joe", "Doe")
val spouse = Person(id = "124", firstName = "Mary", lastName = "Moe")
val joeMarried = joe.copy(lastName = "Doe-Moe")
因此,如果您希望开发人员采用不变性,这就是您可能会考虑转向更现代的编程语言的原因之一。
不可变意味着您不能更改值,而可变意味着如果您考虑原语和对象,则可以更改值。对象与Java中的基元不同,因为它们是内置类型的。基元以int,boolean和void等类型构建。
许多人认为在其前面具有最终修饰符的基元和对象变量是不可变的,但是,事实并非如此。所以final几乎并不意味着变量是不变的。查看此链接以获取代码示例:
public abstract class FinalBase {
private final int variable; // Unset
/* if final really means immutable than
* I shouldn't be able to set the variable
* but I can.
*/
public FinalBase(int variable) {
this.variable = variable;
}
public int getVariable() {
return variable;
}
public abstract void method();
}
// This is not fully necessary for this example
// but helps you see how to set the final value
// in a sub class.
public class FinalSubclass extends FinalBase {
public FinalSubclass(int variable) {
super(variable);
}
@Override
public void method() {
System.out.println( getVariable() );
}
@Override
public int getVariable() {
return super.getVariable();
}
public static void main(String[] args) {
FinalSubclass subclass = new FinalSubclass(10);
subclass.method();
}
}
我认为一个很好的理由将是对“真实”可变的“对象”(例如界面窗口)进行建模。我隐约记得曾经读过OOP是在有人尝试编写软件来控制某些货运港口操作时发明的。
Java在许多情况下要求使用可变对象,例如,当您想要对访问者或可运行的对象进行计数或返回时,即使没有多线程,也需要最终变量。
final
实际上大约是迈向不变的1/4 。没有看到你为什么要提到它。
final
。把它在这方面想起了一个非常奇怪的归并final
与“可变”,当的很点final
是为了防止某些类型的突变。(顺便说一句,不是我的