今天上课时,我的教授正在讨论如何组建课程。该课程主要使用Java,我拥有比老师更多的Java经验(他来自C ++背景),所以我提到在Java中应该支持不变性。我的教授让我证明我的答案是正确的,我说出了我从Java社区听到的原因:
教授质疑我的陈述,说他希望看到对这些好处的一些统计测量。我引用了大量的轶事证据,但即使我这样做,我也意识到他是对的:据我所知,还没有一项关于不变性是否真正提供它在实际代码中所承诺的好处的实证研究。我知道这取决于经验,但其他人的经历可能会有所不同。
所以,我的问题是,是否对现实世界代码中不变性的影响进行过任何统计研究?
答案 0 :(得分:5)
我会在Effective Java中指向Item 15。不变性的价值在于设计(它并不总是合适的 - 它只是一个很好的初步近似),并且从统计的角度来看设计偏好很少被论证,但我们已经看到了可变对象(日历,日期)已经非常糟糕,严重的替代品(JodaTime,JSR-310)选择了不变性。
答案 1 :(得分:2)
在我看来,Java中不变性的最大优点是简单性。如果该状态不能改变,则推理对象状态变得更加简单。这在多线程环境中当然更为重要,但即使在简单的线性单线程程序中,它也可以使事情变得更容易理解。
有关更多示例,请参阅this page。
答案 2 :(得分:2)
所以,我的问题是,有没有 对此进行的任何统计研究 现实世界中不变性的影响 码?
我认为你的教授只是迟钝 - 不一定是有意甚至是坏事。只是问题太模糊了。这个问题有两个真正的问题:
为了它的价值,编译器优化不可变对象的能力已被充分记录。在我的头顶:
map f . map g
转换为map f . g
。由于Haskell函数是不可变的,因此保证这些表达式产生等效输出,但第二个函数的运行速度是原来的两倍,因为我们不需要创建中间列表。x = foo(12); y = foo(12)
是纯函数的情况下,我们才能将temp = foo(12); x = temp; y = temp;
转换为foo
的公共子表达式消除。据我所知,D编译器可以使用pure
和immutable
关键字执行这样的替换。如果我没记错的话,一些C和C ++编译器会积极地优化对这些标记为“pure”的函数的调用(或者等效的关键字)。关于并发性,使用可变状态的并发性缺陷已有详细记录,无需重述。
当然,这都是轶事证据,但这几乎是你得到的最好的证据。不可改变与可变争论在很大程度上是一场小便,你不会发现一篇论文,如“函数式编程优于命令式编程”这样的全面概括。
最多,您可能会发现,您可以在一组最佳实践中汇总不可变vs变异的好处,而不是编码的研究和统计。例如,可变状态是多线程编程的敌人;另一方面,可变队列和数组通常比它们不可变的变体更容易编写和更有效。
这需要练习,但最终你会学会使用正确的工具来完成工作,而不是将你最喜欢的宠物范例用于项目。
答案 3 :(得分:1)
我认为你的教授过于顽固(可能是故意的,以促使你更全面地理解)。实际上,不可变性的好处并不在于编译器可以对优化做些什么,但实际上,我们人类更容易阅读和理解。保证在创建对象时保证设置并且保证在之后不会更改的变量比使用此值现在但可能设置为其他值的更容易理解和推理值以后。
对于线程尤其如此,因为当语言保证不会发生这样的修改时,您不需要担心处理器缓存和监视器以及避免并发修改所带来的所有样板。
一旦你将不变性的好处表达为“代码更易于遵循”,那么要求对“更容易遵循”的生产率提高进行经验测量会感觉有点愚蠢。
另一方面,编译器和Hotspot可能会基于知道值永远不会改变而执行某些优化 - 就像你我感觉这会发生并且是好事但我不确定细节。很可能会有可能发生的优化类型的经验数据,以及生成的代码的速度。
答案 4 :(得分:1)
答案 5 :(得分:1)
你会客观地衡量什么?可以使用相同程序的可变/不可变版本来测量GC和对象计数(尽管那是多么典型的主观,因此这是一个相当弱的论点)。我无法想象你如何能够测量线程错误的消除,除非通过与生成应用程序的现实世界示例进行比较,通过添加不变性来解决间歇性问题,这可能是一种传闻。
答案 6 :(得分:0)
不可变性对于价值对象来说是件好事。但其他事情怎么样?想象一个创建统计数据的对象:
Stats s = new Stats ();
... some loop ...
s.count ();
s.end ();
s.print ();
应该打印“Processed 536.21 rows / s”。您打算如何使用不可变的方法实现count()
?即使您为计数器本身使用不可变值对象,s
也不能是不可变的,因为它必须替换自身内部的计数器对象。唯一的出路是:
s = s.count ();
表示为循环中的每一轮复制s
的状态。虽然这可以做到,但它肯定不如递增内部计数器那么有效。
此外,大多数人都不能正确使用这个API,因为他们希望count()
修改对象的状态而不是返回一个新的。所以在这种情况下,它会产生更多错误。
答案 7 :(得分:0)
正如其他评论所声称的那样,收集有关不可变对象的优点的统计数据非常非常困难,因为找到控制案例几乎是不可能的 - 各种软件应用程序都是相似的,除了一个使用不可变对象而另一个不使用。 (几乎在每种情况下,我都声称该软件的一个版本是在一个版本之后编写的,并从第一个版本中学到了很多教训,因此性能的提高会有很多原因。)任何有经验的程序员都会考虑这个问题。片刻应该意识到这一点。我认为你的教授试图转移你的建议。
与此同时,很容易做出有利于不变性的有力论据,至少在Java中,可能在C#和其他OO语言中。正如Yishai所说, Effective Java 可以很好地解决这个问题。那些实践中的Java并发的副本也在我的书架上。
答案 8 :(得分:0)
不可变对象允许通过共享引用来共享对象的值的代码。但是,可变对象具有通过共享引用来共享对象的身份的代码的标识。在大多数应用中,两种共享都是必不可少的。如果没有可用的不可变对象,则可以通过将值复制到由这些值的预期接收者提供的新对象或对象来共享值。让我没有可变对象要困难得多。人们可以通过说stateOfUniverse = stateOfUniverse.withSomeChange(...)
来“伪造”可变对象,但是当stateOfUniverse
方法正在运行时[排除任何类型的多线程]时,不需要修改withSomeChange
。此外,如果有一个例如试图跟踪一批卡车,并且部分代码对一辆特定的卡车感兴趣,因此该代码必须始终在卡车的桌子上随时查看该卡车。
更好的方法是将Universe细分为实体和值。实体具有可变的特征,但是具有不可变的标识,因此存储位置例如是即使卡车本身改变位置,装载和卸载货物等,类型Truck
也可以继续识别同一辆卡车。价值通常不具有特定的标识,但具有不可变的特征。 Truck
可能会将其位置存储为WorldCoordinate
类型。代表45.6789012N 98.7654321W的WorldCoordinate
将继续存在,只要对它的任何引用存在;如果位于该位置的卡车稍微向北移动,则会创建一个新的WorldCoordinate
代表45.6789013N 98.7654321W,放弃旧的,并存储对该新卡车的引用。
当一切都封装了不可变值或不可变身份时,以及当应该具有不可变身份的事物是可变的时,通常最容易推理代码。如果一个人不想在变量stateOfUniverse
之外使用任何可变对象,更新卡车的位置将需要以下内容:
ImmutableMapping<int,Truck> trucks = stateOfUniverse.getTrucks();
Truck myTruck = trucks.get(myTruckId);
myTruck = myTruck.withLocation(newLocation);
trucks = trucks.withItem(myTruckId,myTruck);
stateOfUniverse = stateOfUniverse.withTrucks(trucks);
但是对该代码的推理将比以下更难:
myTruck.setLocation(newLocation);