为什么我们需要不可变的类?

Rak*_*yal 73 java design-patterns immutability

我无法得到我们需要一个不可变类的场景.
你有没有遇到过这样的要求?或者你可以给我们任何一个我们应该使用这种模式的真实例子.

Ber*_*t F 71

其他答案似乎集中在解释为什么不变性是好的.这是非常好的,我尽可能使用它. 但是,这不是你的问题.我会一点一点地提出你的问题,试着确保你得到你需要的答案和例子.

我无法得到我们需要一个不可变类的场景.

"需要"是这里的相对术语.不可变类是一种设计模式,与任何范例/模式/工具一样,可以使构建软件更容易.类似地,在OO范例出现之前编写了大量代码,但在"需要" OO 的程序员中算作我.像OO这样的不可变类不是严格需要的,但我会表现得像我需要它们.

你有没有遇到过这样的要求?

如果您没有使用正确的透视图查看问题域中的对象,则可能看不到对不可变对象的要求.如果您不熟悉何时有利地使用它们,可能很容易认为问题域不需要任何不可变类.

我经常使用不可变类,我将问题域中的给定对象视为值或固定实例.这个概念有时取决于视角或观点,但理想情况下,很容易切换到正确的视角来识别好的候选对象.

通过确保阅读各种书籍/在线文章,您可以更好地了解不可变对象在哪里真正有用(如果不是绝对必要的话),以便对如何考虑不可变类进行良好的理解.让你开始的一篇好文章是Java理论和实践:改变或不改变?

我将尝试在下面举几个例子,说明如何在不同的视角(可变与不可变)中看到对象,以澄清我的观点.

...请你给我们任何一个我们应该使用这种模式的真实例子.

既然你问过真实的例子我会给你一些,但首先让我们从一些经典的例子开始.

经典价值对象

字符串和整数通常被认为是值.因此,发现String类和Integer包装类(以及其他包装类)在Java中是不可变的并不奇怪.颜色通常被认为是一个值,因此是不可变的Color类.

相比之下,汽车通常不被认为是一种价值对象.对汽车进行建模通常意味着创建一个具有变化状态(里程表,速度,燃料水平等)的类.但是,在某些领域,汽车可能是一个价值对象.例如,汽车(或特别是汽车模型)可能被认为是app中的价值对象,用于查找给定车辆的适当机油.

扑克牌

有没有写过扑克牌计划?我做到了.我本来可以把一张扑克牌代表一个可变的对象,有着可变的套装和等级.一场平局,牌手可能是5种固定的情况下,凡在我的手更换5号卡将意味着通过改变其花色和等级高德变异第五扑克牌实例放入一张新卡.

不过,我倾向于认为扑克牌作为具有固定不变的西装,排名一度创造了一个不可变对象.我画的一手牌是5个实例,并在我的手更换卡将涉及丢弃这些实例之一,并添加一个新的随机实例我的手.

地图投影

最后一个例子是当我处理一些地图代码时,地图可以在各种投影中显示自己.原始代码使地图使用固定但可变的投影实例(如上面的可变扑克牌).更改地图投影意味着改变地图的投影实例的ivars(投影类型,中心点,缩放等).

但是,如果我将投影视为不可变值或固定实例,我觉得设计更简单.更改地图投影意味着让地图引用不同的投影实例,而不是改变地图的固定投影实例.这也使得捕获命名投影变得更加简单MERCATOR_WORLD_VIEW.


Pét*_*rök 42

不可变类通常更容易设计,实现和正确使用.一个例子是String:实现java.lang.Stringstd::stringC++中的实现简单得多,主要是由于它的不变性.

不可变性在一个特别重要的区域是并发性:不可变对象可以安全地在多个线程之间共享,而可变对象必须通过精心设计和实现来实现线程安全 - 通常这远非一项微不足道的任务.

更新: Effective Java 2nd Edition详细解决了这个问题 - 请参阅第15项:最小化可变性.

另见这些相关帖子:


Tar*_*ski 38

Joshua Bloch撰写的有效Java概述了编写不可变类的几个原因:

  • 简单 - 每个班级只在一个州
  • 线程安全 - 因为状态无法更改,所以不需要同步
  • 以不可变的样式编写可以产生更强大的代码.想象一下,如果字符串不是一成不变的; 任何返回String的getter方法都需要实现在返回String之前创建一个防御性副本 - 否则客户端可能会意外或恶意地破坏该对象的状态.

通常,优良作法是使对象不可变,除非结果存在严重的性能问题.在这种情况下,可变构建器对象可用于构建不可变对象,例如StringBuilder


Kir*_*oll 14

Hashmaps是一个典型的例子.地图的关键点必须是不可变的.如果密钥不是不可变的,并且您更改了密钥上的值,使得hashCode()将导致新值,则映射现在已损坏(密钥现在位于哈希表中的错误位置.).

  • 我宁愿说密钥不能改变是必要的; 虽然没有官方要求它是不可改变的. (3认同)
  • 例如http://download.oracle.com/javase/6/docs/api/java/util/Map.html:"注意:如果可变对象用作地图键,必须非常小心".也就是说,可变对象_可以用作键. (2认同)

Bal*_*usC 8

Java实际上是一个和所有引用.有时会多次引用实例.如果更改此类实例,它将反映到其所有引用中.有时你根本不想让它提高稳健性和线程安全性.然后,一个不可变类是有用的,因此强制一个人创建一个实例并将其重新分配给当前引用.这样,其他引用的原始实例保持不变.

想象一下,如果Java String是可变的,它会是什么样子.

  • 或者,如果`Date`和`Calendar`是可变的.哦,等等,他们是,OH SH (12认同)
  • @Salandur:然后它不是"JRE实现".这是一个类似于JRE的实现,但事实并非如此. (6认同)

Dam*_*ver 6

我们本身不需要不可变类,但它们肯定可以使一些编程任务更容易,尤其是涉及多个线程时.您不必执行任何锁定即可访问不可变对象,并且您已经建立的有关此类对象的任何事实将来都将继续存在.


Jay*_*Jay 6

我们来看一个极端情况:整数常量.如果我写一个像"x = x + 1"这样的语句,我想成为100%的知己,数字"1"不会以某种方式变成2,无论程序中的其他地方发生什么.

现在没关系,整数常量不是一个类,但概念是一样的.假设我写道:

String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);
Run Code Online (Sandbox Code Playgroud)

看起来很简单.但是如果Strings不是不可变的,那么我将不得不考虑getCustomerName可能改变customerId的可能性,这样当我调用getCustomerBalance时,我得到了另一个客户的余额.现在你可能会说,"为什么世界上会有人写一个getCustomerName函数让它改变id?这没有任何意义." 但这正是你遇到麻烦的地方.编写上述代码的人可能会明白函数不会更改参数.然后有人出现,他必须修改该功能的另一种用途,以处理客户在同一名称下拥有多个帐户的情况.他说,"哦,这是一个方便的getCustomer名称功能,已经在查找名称了.我

不变性只是意味着某类对象是常量,我们可以将它们视为常量.

(当然用户可以为变量分配一个不同的"常量对象".有人可以编写String s ="hello";然后写s ="goodbye";除非我将变量设为final,否则我无法确定它在我自己的代码块中没有被改变.就像整数常量一样,我确保"1"总是相同的数字,但是不能通过写"x = 2"来改变"x = 1".但是我如果我有一个不可变对象的句柄,那么我可以知道没有我传递给它的函数可以在我身上改变它,或者如果我制作了它的两个副本,那么对一个副本的变量的更改将不会改变其他.等等


Buh*_*ndi 5

不可变性有多种原因:

  • 线程安全:不可变对象也不能更改,内部状态也不能更改,因此无需同步.
  • 它还保证我通过(通过网络)发送的任何内容必须与先前发送的状态相同.这意味着没有人(窃听者)可以在我的不可变集中添加随机数据.
  • 它的开发也比较简单.如果对象是不可变的,则保证不存在子类.比如一个String班级.

因此,如果您想通过网络服务发送数据,并且希望保证您的结果与发送的结果完全相同,请将其设置为不可变.


JUS*_*ION 5

我将从另一个角度来攻击这个问题.我发现不可变对象在阅读代码时让我的生活更轻松.

如果我有一个可变对象,我永远不确定它的价值是什么,如果它曾在我的直接范围之外使用过.假设我MyMutableObject在方法的局部变量中创建,用值填充它,然后将其传递给其他五个方法.这些方法中的任何一个都可以改变我的对象的状态,因此必须发生以下两种情况之一:

  1. 在考虑我的代码逻辑时,我必须跟踪另外五种方法的主体.
  2. 我必须为我的对象制作五个浪费的防御副本,以确保将正确的值传递给每个方法.

第一个让我的代码难以推理.第二个使得我的代码在性能上很糟糕 - 我基本上模仿了一个带有写时复制语义的不可变对象,但无论被调用的方法是否实际修改了我的对象的状态,它总是在做.

如果我改为使用MyImmutableObject,我可以放心,我设置的是我的方法生命周期的价值.没有"远距离的怪异动作"会从我的身下改变它,并且在调用其他五种方法之前我不需要制作我的物体的防御性副本.如果其他方法想要为了他们的目的而改变事物,他们必须制作副本 - 但是如果他们真的必须复制(而不是我在每次外部方法调用之前这样做),他们只会这样做.我不遗余力地跟踪那些甚至可能不在我当前源文件中的方法,并且我为系统节省了无休止地制作不必要的防御副本以防万一.

(如果我走出Java世界,进入C++世界等等,我可能会变得更加棘手.我可以让对象看起来好像是可变的,但是在幕后让它们透明地克隆在任何对象上.一种状态变化 - 即写作时的复制 - 没有人是明智的.)


hag*_*wal 5

我给未来访客的 2 美分:


不可变对象是不错选择的 2 个场景是:

在多线程

多线程环境中的并发问题可以通过同步很好地解决,但同步是昂贵的事情(不会在这里挖掘“为什么”),所以如果您使用不可变对象,那么没有同步来解决并发问题,因为状态不可变对象无法更改,如果状态无法更改,则所有线程都可以无缝访问该对象。因此,不可变对象是多线程环境中共享对象的绝佳选择。


作为基于哈希的集合的键

使用基于散列的集合时要注意的最重要的事情之一是,键应该hashCode()在对象的生命周期内始终返回相同的值,因为如果该值发生更改,则旧条目会进入基于散列的集合使用该对象无法检索,因此会导致内存泄漏。由于不可变对象的状态无法更改,因此它们是基于散列集合中的键的绝佳选择。因此,如果您使用不可变对象作为基于散列的集合的键,那么您可以确保不会因此出现任何内存泄漏(当然,当用作键的对象未被从任何地方引用时,仍然可能存在内存泄漏否则,但这不是这里的重点)。