为什么我们需要不可变的类?
我无法得到我们需要一个不可变类的场景。
你有没有遇到过这样的要求? 还是可以给我们一个真实的例子,我们应该使用这种模式。
其他答案似乎集中在解释为什么不变性是好的。 这是非常好的,我尽可能使用它。 但是,这不是你的问题 。 我将逐点提出您的问题,以确保您获得所需的答案和示例。
我无法得到我们需要一个不可变类的场景。
“需要”是这里的一个相对术语。 不可变类是一种devise模式,像任何范式/模式/工具一样,可以使构build软件变得更容易。 同样,在面向对象范式出现之前写了大量的代码,但是在程序员中需要计算“需要”面向对象的代码。 像OO这样的不可变类不是严格需要的 ,但是我会按照我需要的那样行事。
你有没有遇到过这样的要求?
如果您没有以正确的视angular查看问题域中的对象,则可能看不到不可变对象的需求 。 如果您不熟悉何时使用它们,可能会很容易认为问题域不需要任何不可变类。
我经常使用不可变的类,我把我的问题域中给定的对象看作是一个值或固定的实例 。 这个概念有时候依赖于观点或观点,但理想情况下,很容易转换到正确的angular度来确定好的候选对象。
通过确保阅读各种书籍/在线文章,可以更好地理解不可变对象的真正用处 (如果不是绝对必要的话),从而为如何思考不可变类提供良好的意义。 一个很好的文章让你开始Java理论和实践:突变或不突变?
我将尝试下面的几个例子来说明如何用不同的angular度来看待对象(可变还是不可变),以澄清我的观点。
…请给我们一个真实的例子,我们应该使用这种模式。
既然你要求真实的例子,我会给你一些,但首先让我们开始一些经典的例子。
经典的价值对象
string和整数,经常被认为是价值观。 因此,发现String类和Integer包装类(以及其他包装类)在Java中是不可变的,这并不奇怪。 一个颜色通常被认为是一个值,因此是不变的Color类。
反
相反,一辆汽车通常不被看作是一个价值对象。 对汽车进行build模通常意味着创build一个具有不断变化的状态(里程表,速度,油位等)的类。 然而,汽车可能是一个价值对象。 例如,一辆汽车(或特别是一辆汽车模型)可能被认为是一个应用程序的价值对象,以查找给定的车辆适当的机油。
玩纸牌
曾经写过一个扑克牌程序? 我做了。 我本可以把一张纸牌当作一个可变的衣服和等级的可变物。 一个抽牌手可以是5个固定的例子,在我手中replace第5张牌意味着通过改变它的套装和等级ivars将第5张纸牌实例变成新的卡。
不过,我倾向于把扑克牌看作是一个固定不变的套装和等级,一旦创build就是一个不变的物体。 我的抽牌手牌会是5次,手中的一张牌将会丢弃其中的一张,并且会随机添加一个新的实例。
地图投影
最后一个例子是当我在一些地图代码上工作时,地图可以在各种投影中显示。 原始代码有地图使用固定的,但可变的投影实例(如上面的可变的纸牌)。 改变地图投影意味着改变地图的投影实例的ivars(投影types,中心点,缩放等)。
不过,如果我把投影看成一个不变的值或固定的实例,我觉得devise更简单。 改变地图投影意味着让地图引用一个不同的投影实例,而不是改变地图的固定投影实例。 这也使捕获命名投影(如MERCATOR_WORLD_VIEW
变得更简单。
不可变类通常要简单得多,devise,实现和正确使用 。 一个例子是String:在C ++中java.lang.String
的实现比std::string
简单得多,主要是由于它的不变性。
一个特别的区域,不可变性会造成一个特别大的差异:并发性: 不可变对象可以安全地在多个线程之间共享 ,而可变对象必须通过仔细的devise和实现而成为线程安全的 – 通常这远不是一件微不足道的任务。
更新: 有效的Java第二版详细解决这个问题 – 请参阅项目15:最小化可变性 。
另请参阅这些相关的post:
- 具有stringtypes不可变的非技术优势
- 在Java中的不可变对象下降?
Joshua Bloch撰写的Effective Java概述了编写不可变类的几个原因:
- 简单 – 每个class只在一个状态
- 线程安全 – 因为状态不能改变,所以不需要同步
- 以不可改变的风格编写可以产生更健壮的代码。 想象一下,如果string不是不变的, 任何返回String的getter方法都要求实现在返回String之前创build一个防御副本,否则客户端可能会意外或恶意破坏对象的状态。
一般来说,除非出现严重的性能问题,否则使对象成为不可变的是一种很好的做法。 在这种情况下,可变的构build器对象可以用来构build不可变的对象,比如StringBuilder
散列图是一个典型的例子。 地图的关键是不可改变的。 如果密钥不是不可变的,并且你改变密钥的值,使得hashCode()会产生一个新值,那么地图现在被破坏了(密钥现在在哈希表的错误位置)。
我们本身并不需要不可变的类,但是它们当然可以使一些编程任务更容易,特别是当涉及多个线程时。 您不必执行任何locking来访问不可变的对象,并且您已经build立的关于这样的对象的任何事实在未来将继续保持真实。
Java实际上是一个和所有的参考。 有时一个实例被多次引用。 如果你改变了这样一个实例,它将被反映到所有的引用中。 有时你根本不希望这样做能提高健壮性和线程安全性。 然后,一个不可变的类是有用的,所以被迫创build一个新的实例,并重新分配给当前的引用。 这样,其他引用的原始实例保持不变。
想象一下,如果String
是可变的,Java将如何看起来像。
不变性有多种原因:
- 线程安全:不可变的对象不能改变,内部状态也不能改变,所以不需要同步。
- 它还保证,无论我通过networking发送(无论通过networking),都必须与之前发送的状态相同。 这意味着没有人(窃听者)可以来我的不变的集合中添加随机数据。
- 开发也比较简单。 你保证如果一个对象是不可变的,那么不存在任何子类。 例如一个
String
类。
因此,如果您想通过networking服务发送数据,而且您希望保证您的结果与发送的结果完全一致,请将其设置为不可变。
让我们来看一个极端的例子:整型常量。 如果我写一个像“x = x + 1”这样的语句,我希望100%知道,数字“1”不会变成2,不pipe程序中的其他地方发生了什么。
现在好吧,整数常量不是一个类,但概念是相同的。 假设我写:
String customerId=getCustomerId(); String customerName=getCustomerName(customerId); String customerBalance=getCustomerBalance(customerid);
看起来很简单。 但是,如果string不是不可变的,那么我将不得不考虑getCustomerName可能改变customerId的可能性,所以当我调用getCustomerBalance时,我得到了另一个客户的余额。 现在你可能会说:“为什么在这个世界上有人写一个getCustomerName函数使得它改变了ID?这是没有意义的。 但那正是你可能遇到麻烦的地方。 编写上述代码的人可能会认为这些function不会改变参数。 然后有人出现,必须修改该function的另一个用途,以处理客户有多个同名的账户的情况。 他说,“哦,这个方便的getCustomer名称函数已经在查找这个名字了,我只会自动把这个id改成下一个同名的账号,然后把它放在一个循环中。那么你的程序开始神秘不工作。 这是不好的编码风格? 大概。 但是在副作用不明显的情况下恰恰是一个问题。
不变性只是意味着某一类对象是常量,我们可以把它们看作常量。
(当然用户可以给一个variables分配一个不同的“常量对象”,有人可以写String s =“hello”,然后写s =“goodbye”;除非我把variables做成final,否则我不能确定它不会在我自己的代码块中被改变,就像整型常量保证“1”总是相同的数字,但不是写“x = 2”不会改变“x = 1”。可以知道,如果我有一个不可变对象的句柄,我没有把它传递给我可以改变它,或者如果我做了两个副本,对variables持有一个副本的变化不会改变其他。
我将从另一个angular度来攻击这个问题。 在阅读代码时,我发现不可变的对象使我的生活更轻松。
如果我有一个可变的对象,我永远不知道它的价值是什么,如果它曾经用在我的眼前的范围之外。 比方说,我在方法的局部variables中创buildMyMutableObject
,用值填充它,然后将其传递给其他五个方法。 任何一种方法都可以改变我的对象的状态,所以有两件事情必须发生:
- 在考虑我的代码逻辑的同时,我必须跟踪另外五个方法的实体。
- 我必须做五个浪费的防御性的副本,以确保正确的值传递给每个方法。
第一个推理我的代码很困难。 第二个让我的代码在性能上很糟糕 – 我基本上是用模仿copy-on-write语义的不可变对象,但是不pipe被调用的方法是否实际修改我的对象的状态。
如果我使用MyImmutableObject
,我可以放心,我所设定的是我的方法的生命值。 没有任何“远距离的鬼怪行动”,将会从我的下面改变它,并且在调用其他五种方法之前,我不需要对我的对象进行防御性的拷贝。 如果其他方法想为了他们的目的而改变他们的东西, 他们必须做这个副本,但是如果他们真的必须做一个副本(而不是在每次外部方法调用之前这样做)。 我不遗余力地logging那些甚至可能不在我目前的源文件中的方法的心理资源,并且为了以防万一,让系统免去不必要的防御副本的开销。
(如果我走出Java世界,进入C ++世界等等,我可以变得更加棘手,我可以让这些对象看起来像是可变的,但是在后台使它们透明地克隆一种状态改变 – 即写复制 – 没有人比较聪明。)
使用final关键字不一定会使某些不可变的东西:
public class Scratchpad { public static void main(String[] args) throws Exception { SomeData sd = new SomeData("foo"); System.out.println(sd.data); //prints "foo" voodoo(sd, "data", "bar"); System.out.println(sd.data); //prints "bar" } private static void voodoo(Object obj, String fieldName, Object value) throws Exception { Field f = SomeData.class.getDeclaredField("data"); f.setAccessible(true); Field modifiers = Field.class.getDeclaredField("modifiers"); modifiers.setAccessible(true); modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL); f.set(obj, "bar"); } } class SomeData { final String data; SomeData(String data) { this.data = data; } }
只是一个例子来certificate“最终”关键字是为了防止程序员错误,而不是更多。 而重新分配缺乏最终关键字的价值很容易偶然发生,而要改变这个价值则必须有意识地进行。 这是为了文档和防止程序员的错误。
不可变的数据结构在编码recursionalgorithm时也可以有所帮助。 例如,假设您正在尝试解决3SAT问题。 一种方法是做到以下几点:
- select一个未分配的variables。
- 给它的值为TRUE。 通过取出现在满足的子句来简化实例,并重新解决更简单的实例。
- 如果TRUE情况下的recursion失败,则改为赋值variablesFALSE。 简化这个新的实例,并再次解决它。
如果你有一个可变结构来表示这个问题,那么当你简化TRUE分支中的实例时,你必须:
- 跟踪您所做的所有更改,并在您意识到问题无法解决后全部撤消。 这有很大的开销,因为你的recursion可能会非常深入,编码很难。
- 制作实例副本,然后修改副本。 这将是缓慢的,因为如果你的recursion深度是几十个层次,你将不得不做很多副本的实例。
但是,如果你以一种聪明的方式编写代码,你可以有一个不可变的结构,其中任何操作返回一个更新(但仍然是不可变的)版本的问题(类似于String.replace
– 它不replacestring,只是给你一个新的)。 实现这一点的天真的方法是让“不可变”的结构在任何修改中复制并创build一个新的结构,当它有一个可变的结构时,将其减less到第二个解决scheme,所有的开销,但是你可以在更多高效的方式。
“需要”不可变类的原因之一是通过引用传递所有内容,而不支持对象的只读视图(即C ++的const
)。
考虑一个支持观察者模式的类的简单情况:
class Person { public string getName() { ... } public void registerForNameChange(NameChangedObserver o) { ... } }
如果string
不是不可变的, Person
类不可能正确地实现registerForNameChange()
,因为有人可以写下面的内容,有效地修改这个人的名字而不会触发任何通知。
void foo(Person p) { p.getName().prepend("Mr. "); }
在C ++中, getName()
返回一个const std::string&
,通过引用返回并阻止访问mutators,这意味着不可变类在该上下文中不是必需的。
他们也给我们保证。 不可变性的保证意味着我们可以扩展它们,创造新的效率模式,否则就无法实现。
尚未调用的不可变类的一个特征是:存储对深度不可变类对象的引用是存储其中包含的所有状态的有效方法。 假设我有一个可变对象,它使用一个深不可变的对象来保存50K的状态信息。 进一步假设,我希望在25次时对原始(可变)对象进行“复制”(例如,对于“撤消”缓冲区)。 状态可以在复制操作之间改变,但通常不会。 制作可变对象的“副本”只需要将引用复制到其不可变状态,所以20个副本只需要20个引用。 相比之下,如果州政府价值5万元的可变物体,25个副本中的每一个都必须自己制作50K的数据; 拿着所有25份拷贝将需要持有大部分重复的数据。 即使第一个复制操作会产生一个永远不会改变的数据的副本,而其他的24个操作理论上可以简单地回顾一下,在大多数实现中,第二个对象不会要求复制知道一个不可变的副本已经存在的信息(*)。
(*)有时候可能有用的一种模式是可变对象有两个字段来保存它们的状态 – 一个是可变forms,另一个是不可变forms。 对象可以被复制为可变或不可变的,并且可以从一个或另一个参考集开始。 只要对象想要改变它的状态,它就会将不可变引用复制到可变引用(如果还没有完成的话)并且使不可变引用无效。 当对象被复制为不可变时,如果它的不可变引用没有被设置,将会创build一个不可变的副本,并且不可变的引用指向那个对象。 这种方法将需要比“完全写入的拷贝”多一些的复制操作(例如,要求复制自从最后一个拷贝需要复制操作以来已经被变异的对象,即使原始对象不再被变异),但它避免了FFCOW带来的线程复杂性。
来自Effective Java; 一个不可变的类只是一个其实例不能被修改的类。 包含在每个实例中的所有信息在创build时提供,并且在对象的生命周期中被固定。 Java平台库包含许多不可变的类,包括string,盒装原始类,BigInteger和BigDecimal。 有很多很好的理由:不可变类比可变类更容易devise,实现和使用。 他们不太容易出错,更安全。
通过不变性,您可以确定行为不会发生变化,您将获得执行额外操作的额外优势:
-
您可以轻松地使用多个核心/处理( 并发处理 )(因为序列不再重要)。
-
可以做昂贵的操作caching (你肯定是一样的
结果)。 -
可以放心地进行debugging (因为运行的历史不会被关注
了)