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

时间:2010-09-22 13:17:59

标签: java design-patterns immutability

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

19 个答案:

答案 0 :(得分:72)

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

  

我无法得到需要不可变类的场景。

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

  

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

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

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

通过确保阅读各种书籍/在线文章,您可以更好地了解不可变对象真正有用的地方(如果不是绝对必要的话),以便对如何思考有良好的认识关于不可变类。让你入门的一篇好文章是Java theory and practice: To mutate or not to mutate?

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

  

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

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

经典值对象

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

<强>反例

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

扑克牌

有没有写过扑克牌计划?我做到了。我本来可以把一张扑克牌代表一个可变的对象,有着可变的套装和等级。一个平局扑克牌可以是5个固定的实例,其中替换我手中的第5张牌将意味着通过更改其套装和等级ivars将第5张扑克牌实例变为新牌。

但是,我倾向于认为扑克牌是一个不可变的对象,它具有固定的不变的套装和等级一旦创建。我的抽奖扑克手牌将是5个实例,而我手中的牌替换将丢弃其中一个实例并在我手中添加一个新的随机实例。

地图投影

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

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

答案 1 :(得分:41)

一般来说,不可变类很容易设计,实现和正确使用。一个例子是String:java.lang.String的实现比C ++中std::string的实现简单得多,主要是因为它的不变性。

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

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

另见这些相关帖子:

答案 2 :(得分:38)

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

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

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

答案 3 :(得分:12)

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

答案 4 :(得分:8)

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

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

答案 5 :(得分:6)

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

答案 6 :(得分:5)

不可变性有多种原因:

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

因此,如果您想通过网络服务发送数据,并且想要感觉保证,那么您的结果将与发送的结果完全相同,请将其设置为不可变。< / p>

答案 7 :(得分:5)

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

现在好了,整数常量不是一个类,但概念是一样的。假设我写道:

String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);

看起来很简单。但是如果Strings不是不可变的,那么我将不得不考虑getCustomerName可能改变customerId的可能性,这样当我调用getCustomerBalance时,我得到了另一个客户的余额。现在你可能会说,“为什么世界上会有人写一个getCustomerName函数让它改变id?这没有任何意义。”但这正是你遇到麻烦的地方。编写上述代码的人可能会明白函数不会更改参数。然后有人出现,他必须修改该功能的另一种用途,以处理客户在同一名称下拥有多个帐户的情况。他说,“哦,这是一个方便的getCustomer名称功能,它已经在查找名称了。我只是自动将id更改为下一个具有相同名称的帐户,并将其置于循环中......”然后你的程序开始神秘地不起作用。这会是糟糕的编码风格吗?大概。但在副作用不明显的情况下,这恰恰是一个问题。

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

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

答案 8 :(得分:5)

我将从不同的角度对此进行攻击。我发现不可变对象在阅读代码时让我的生活更轻松。

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

  1. 在考虑我的代码逻辑时,我必须跟踪其他五种方法的主体。
  2. 我必须为我的对象制作五个浪费的防御副本,以确保将正确的值传递给每个方法。
  3. 第一个让我的代码难以理解。第二个让我的代码在性能上很糟糕 - 我基本上模仿了一个带有copy-on-write语义的不可变对象,但无论被调用的方法是否实际修改了我的对象的状态,它总是在做它。

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

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

答案 9 :(得分:2)

我给未来访客的2美分:


不可变对象是不错的选择的两种方案是:

在多线程中

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


作为基于哈希的集合的键

使用基于哈希的集合时要注意的最重要的事情之一是,密钥应使其hashCode()在对象的生存期内始终返回相同的值,因为如果该值被更改,则无法检索使用该对象在基于哈希的集合中创建的旧条目,因此将导致内存泄漏。 由于不能更改不可变对象的状态,因此它们是基于哈希的集合中作为键的绝佳选择。因此,如果您将不可变对象用作基于哈希的集合中的键,则可以确保存在因此不会造成任何内存泄漏(当然,当没有从任何其他地方引用用作键的对象时,仍然会存在内存泄漏。

答案 10 :(得分:1)

不可变数据结构在编码递归算法时也有帮助。例如,假设您正在尝试解决3SAT问题。一种方法是执行以下操作:

  • 选择一个未分配的变量。
  • 将其值设为TRUE。通过删除现在满足的子句来简化实例,并重复解决更简单的实例。
  • 如果对TRUE情况的递归失败,则改为将该变量赋值为FALSE。简化这个新实例,并重复解决它。

如果你有一个可变的结构来表示问题,那么当你在TRUE分支中简化实例时,你要么必须:

  • 跟踪您所做的所有更改,并在您意识到问题无法解决后撤消所有更改。这会产生很大的开销,因为你的递归可能非常深入,并且编码很棘手。
  • 制作实例的副本,然后修改副本。这将是缓慢的,因为如果你的递归是几十层深,你将不得不制作许多实例的副本。

但是,如果您以巧妙的方式对其进行编码,则可以使用不可变结构,其中任何操作都会返回问题的更新(但仍然不可变)版本(类似于String.replace - 它不会替换字符串,只给你一个新的)。实现这一点的天真方法是让“不可变”结构只是复制并在任何修改时创建一个新结构,当有一个可变的结构时将其减少到第二个解决方案,并且所有这些开销,但你可以在更多有效的方式。

答案 11 :(得分:1)

不可变类“需要”的原因之一是通过引用传递所有内容并且不支持对象的只读视图(即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()的{​​{1}}具有通过引用返回并阻止访问mutator的效果,这意味着在该上下文中不需要不可变类。

答案 12 :(得分:1)

他们也给了我们保证。不可变性的保证意味着我们可以扩展它们并创建新的效率图案,否则这些图案是不可能的。

http://en.wikipedia.org/wiki/Singleton_pattern

答案 13 :(得分:1)

尚未调出的不可变类的一个特性:存储对深度不可变类对象的引用是存储其中包含的所有状态的有效方法。假设我有一个可变对象,它使用一个深度不可变的对象来保存50K的状态信息。进一步假设,我希望25次对我原始(可变)对象进行“复制”(例如对于“撤销”缓冲区);国家可以在复制操作之间进行更改,但通常不会。制作可变对象的“副本”只需要复制对其不可变状态的引用,因此20个副本只需要20个引用。相比之下,如果国家被保存在价值50K的可变对象中,那么25个复制操作中的每一个都必须生成自己的50K数据副本;持有所有25份副本将需要持有超过大量重复数据。即使第一个复制操作会生成一个永远不会改变的数据副本,其他24个操作理论上可以简单地再次参考,在大多数实现中,第二个对象无法请求副本知道已存在不可变副本的信息(*)。

(*)有时可用的一种模式是可变对象有两个字段来保存它们的状态 - 一个是可变形式,另一个是不可变形式。对象可以被复制为可变或不可变的,并且可以从一个或另一个引用集开始。只要对象想要更改其状态,它就会将不可变引用复制到可变引用(如果尚未完成)并使不可变引用无效。当对象被复制为不可变时,如果未设置其不可变引用,则将创建不可变副本,并且不可变引用指向该对象。这种方法需要比“写入时完整的副本”更多的复制操作(例如,要求复制自上一次复制需要复制操作以来已经变异的对象,即使原始对象永远不会再次发生变异但它避免了FFCOW带来的线程复杂性。

答案 14 :(得分:1)

Immutable objects are instances whose states do not change once initiated. The use of such objects is requirement specific.

Immutable class is good for caching purpose and it is thread safe.

答案 15 :(得分:1)

使用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;
    }
}

只是一个例子来证明“final”关键字可以防止程序员错误,而不是更多。虽然重新分配缺少最终关键字的值很容易发生,但是必须有意地完成这个长度以改变值。它用于文档并防止程序员错误。

答案 16 :(得分:0)

来自Effective Java的

; 不可变类只是一个无法修改其实例的类。所有的 每个实例中包含的信息在创建时提供,并且是 固定为对象的生命周期。 Java平台库包含许多 不可变类,包括String,盒装原始类和BigInte- ger和BigDecimal。这有很多很好的理由:不可变的课程 比可变类更容易设计,实现和使用。它们不太容易发生 错误,更安全。

答案 17 :(得分:0)

凭借不变性,您可以确保底层不可变对象的行为/状态不会发生变化,并且您可以获得执行其他操作的额外优势:

  • 您可以轻松使用多个核心/处理(并发/并行处理)(因为操作序列将不再重要。)

  • 可以执行缓存进行昂贵的操作(因为您确定相同) 结果)。

  • 可以轻松完成调试(因为运行历史不会引起关注) 再)

答案 18 :(得分:0)

  

为何选择Immutable类?

一旦对象被实例化,它的状态就不能在生命周期中改变。这也使线程安全。

  

示例:

显然是String,Integer和BigDecimal等。一旦创建了这些值,就不能在生命周期内改变。

  

用例:   使用其配置值创建数据库连接对象后,您可能不需要更改其可以使用不可变类的状态