为什么我们需要不变的阶级?


83

我无法获得需要不可变类的方案。
您是否曾经遇到过此类要求?或者您可以给我们提供任何实际的示例来说明我们应该使用这种模式的情况。


13
令我惊讶的是,没有人把这个重复。好像你们只是在尝试简单点... :)(1)stackoverflow.com/questions/1284727/mutable-or-immutable-class(2)stackoverflow.com/questions/3162665/immutable-class( 3)stackoverflow.com/questions/752280/...
Javid詹美回教堂

Answers:


82

其他答案似乎太着重于解释为什么不变性很好。它非常好,我会尽可能使用它。 但是,这不是您的问题。我将逐点讨论您的问题,以确保您得到所需的答案和示例。

我无法获得需要不可变类的情况。

“需要”在这里是相对术语。不变类是一种设计模式,就像任何范式/模式/工具一样,在那里可以简化软件的构造。同样,在OO范式问世之前,已经编写了很多代码,但在“需要” OO的程序员中,我却算是其中之一。不严格需要像OO这样的不可变类,但是我将表现得像需要它们。

您是否曾经遇到过此类要求?

如果您未从正确的角度查看问题域中的对象,则可能看不到对不可变对象的需求。如果您不熟悉何时方便地使用它们,可能容易想到问题域不需要任何不可变的类。

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

通过确保阅读各种书籍/在线文章,从而更好地了解不可变类,您可以更好地了解不可变对象在哪些地方真正有用(如果不是严格必要的话)。Java理论和实践是一篇入门的好文章:突变还是不突变?

我将在下面尝试一些示例,说明如何以不同的视角(可变与不可变)查看对象,以阐明我的观点。

...您能给我们一个实际的例子,说明我们应该使用这种模式的地方吗?

由于您要求提供真实的示例,因此我将为您提供一些示例,但首先,让我们从一些经典示例开始。

经典价值对象

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

反例

相反,汽车通常不被视为价值对象。对汽车进行建模通常意味着要创建一个具有变化状态(里程表,速度,燃油水平等)的类。但是,在某些领域中,汽车可能是价值对象。例如,在特定应用程序中,汽车(或具体来说是汽车模型)可能被视为价值对象,以查找适当的机油。

扑克牌

曾经写过纸牌程序吗?是的 我本来可以将扑克牌表示为具有可变西装和等级的可变对象。抽牌手可能是5个固定实例,其中替换我手中的第5张纸牌意味着通过更改其西装和等级使第5张纸牌实例变为新纸牌。

但是,我倾向于将纸牌视为一个不变的对象,一旦创建便具有固定的不变衣服和等级。我的抽奖扑克手为5个实例,而替换我手中的纸牌将涉及丢弃其中一个实例并向我的手添加一个新的随机实例。

地图投影

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

但是,如果我认为投影是不可变的值或固定的实例,则我认为设计会更简单。更改地图投影意味着要使地图参考其他投影实例,而不是使地图的固定投影实例发生变化。这也使捕获诸如的命名投影变得更加简单MERCATOR_WORLD_VIEW


42

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

不变性在其中特别重要的一个方面是并发性:不变对象可以在多个线程之间安全地共享,而可变对象必须通过仔细的设计和实现使其成为线程安全的-通常这并非一件容易的事。

更新: 有效的Java 2nd Edition详细解决了此问题-请参阅第15项:最小化可变性

另请参阅以下相关文章:


39

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

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

通常,除非导致严重的性能问题,否则使对象不可变是一种好习惯。在这种情况下,可变的构建器对象可用于构建不可变的对象,例如StringBuilder


1
每个类处于一个状态还是每个对象?
Tushar Thakur

@TusharThakur,每个对象。
小丑

17

哈希图是一个典型的例子。至关重要的是,映射的键必须是不变的。如果键不是一成不变的,并且您更改键上的值以使hashCode()会产生新值,则映射现在已损坏(键现在在哈希表中的错误位置)。


3
我宁愿不要更改密钥。官方没有要求它是不变的。
彼得Török

不知道您所说的“官方”是什么意思。
柯克·沃尔

1
我认为他的意思是,唯一真正的要求是不得更改键……而不是根本不能更改(通过不变性)。当然,防止密钥被更改的一种简单方法是首先使其不可变!:-)
Platinum Azure

2
例如:download.oracle.com/javase/6/docs/api/java/util/Map.html:“注意:如果将可变对象用作映射键,则必须格外小心”。即,可变对象可以用作键。
彼得Török

8

Java实际上是所有引用之一。有时一个实例被多次引用。如果更改此类实例,它将反映到其所有引用中。有时,您根本不想使用它来提高鲁棒性和线程安全性。然后,一个不可变的类很有用,这样一来就可以强制创建一个实例并将其重新分配给当前引用。这样,其他引用的原始实例将保持不变。

想象一下,如果JavaString是可变的,它将是什么样子。


12
或者,如果DateCalendar是可变的。哦,等等,他们是,OH SH
gustafc

1
@gustafc:考虑到日历进行日期计算的工作(可以通过返回副本来完成,但考虑日历的重量很重,这样更好),日历是可变的就可以了。但是Date-是的,这很讨厌。
Michael Borgwardt 2010年

在某些JRE实现上String是可变的!(提示:一些较旧的JRockit版本)。调用string.trim()会导致原始字符串被修剪
Salandur 2010年

6
@Salandur:那不是“ JRE实现”。它是类似于JRE的东西的实现,但不是。
Mark Peters 2010年

@马克·彼得斯(Mark Peters):一个非常真实的声明
萨兰德(Salandur)2010年

6

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


6

让我们来看一个极端的情况:整数常量。如果我写类似“ x = x + 1”的语句,那么我想100%知道数字“ 1”不会以任何方式变为2,无论程序中其他任何地方发生了什么。

现在好了,整数常量不是类,但是概念是相同的。假设我写:

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

看起来很简单。但是,如果字符串不是一成不变的,那么我将不得不考虑getCustomerName可以更改customerId的可能性,因此,当我调用getCustomerBalance时,将获得其他客户的余额。现在您可能会说:“为什么在世界上编写getCustomerName函数的人可以更改ID?这没有任何意义。” 但这正是您可能会遇到麻烦的地方。编写上述代码的人可能会认为函数显然不会更改参数。然后有人需要修改该功能的另一种用法,以处理客户拥有多个同名帐户的情况。他说:“哦,这是这个方便的getCustomer名称函数,已经在查找名称了。我

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

(当然,用户可以为变量分配一个不同的“常量对象”。有人可以编写String s =“ hello”;然后再编写s =“ goodbye”;除非我将变量定为final,否则我无法确定就像在我自己​​的代码块中没有更改一样,就像整数常量一样,向我保证“ 1”始终是相同的数字,但不是“ x = 1”将永远不会通过写入“ x = 2”来更改。可以自信地说,如果我有一个不可变对象的句柄,那么我传递给它的任何函数都不能对其进行更改,或者如果我对其进行两个副本,则对持有一个副本的变量的更改不会更改其他等等。


5

不变性有多种原因:

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

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


我没有通过网络发送信息的部分,也没有提到不可扩展类不能扩展的部分。
aioobe 2010年

@aioobe,发送数据在网络上,如Web服务,RMI等。而对于延长,则无法扩展的不可变的String类,或ImmutableSet,ImmutableList等
Buhake辛迪

无法扩展String,ImmutableSet,ImmutableList等是与不变性完全正交的问题。并非finalJava中标记的所有类都是不可变的,也不是标记所有的不可变类final
我的正确

5

我将从另一个角度对此进行攻击。我发现不变的对象使我在阅读代码时的生活变得更加轻松。

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

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

首先,很难对我的代码进行推理。第二个因素使我的代码性能下降-无论如何,我基本上是在模仿使用写时复制语义的不可变对象,但是无论调用的方法是否实际上修改了对象的状态,它始终都在模仿。

如果我改用MyImmutableObject,可以放心,我设置的值将是方法生命期内的值。没有“远距离的诡异动作”可以将其从我身下改变出来,也不需要我在调用其他五种方法之前对自己的对象进行防御性复制。如果其他方法想要根据自己的目的进行更改,必须制作副本-但是只有在确实需要制作副本的情况下才这样做(与我在每个外部方法调用之前所做的相反)。我不遗余力地跟踪可能甚至不在当前源文件中的方法,并且为系统以防万一,不断制作不必要的防御性副本的系统开销不遗余力。

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


可变值类型在支持它们并且不允许持久引用的语言中的一个优点是,您获得了所描述的优点(如果可变引用类型是通过引用传递给例程的,则该例程可以在运行时对其进行修改,但在返回后不能引起修改)而没有不可变对象通常具有的一些笨拙之处。
超级猫

5

我给未来访客的2美分:


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

在多线程中

同步可以很好地解决多线程环境中的并发问题,但是同步非常昂贵(这里不介绍“为什么”),因此,如果您使用不可变对象,则没有同步来解决并发问题,因为状态为不变的对象不能更改,如果状态不能更改,则所有线程都可以无缝访问该对象。因此,不可变对象是多线程环境中共享对象的理想选择。


作为基于哈希的集合的键

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


2

不变对象是实例,实例一旦启动就不会改变。此类对象的使用是特定于需求的。

不可变的类非常适合缓存目的,并且是线程安全的。


2

凭借不变性,您可以确保基本不变对象的行为/状态不会改变,并获得执行其他操作的额外优势:

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

  • 可以对昂贵的操作进行缓存(您可以肯定相同的
    结果)。

  • 可以轻松进行调试(因为不再需要运行历史记录


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”关键字可以防止程序员出错,仅此而已。尽管偶然地很容易发生重新分配缺少final关键字的值的情况,但是要花这么长时间更改值必须有意进行。它在那里提供文档和防止程序员错误。


请注意,这不是直接针对最终问题,而是直接回答问题。这是关于不可变的类。您可以使用适当的访问限制在没有final关键字的情况下拥有不可变的类。
马克·彼得斯2010年

1

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

  • 选择一个未分配的变量。
  • 将其值设为TRUE。通过取出现在满足的子句来简化实例,然后再次解决更简单的实例。
  • 如果在TRUE情况下递归失败,则改为分配该变量FALSE。简化此新实例,然后再次解决它。

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

  • 跟踪所做的所有更改,并在发现问题无法解决后将其全部撤消。这有很大的开销,因为您的递归可以进行得很深,并且编写代码很棘手。
  • 制作实例的副本,然后修改副本。这会很慢,因为如果您的递归深度达到几十个级别,则必须制作许多实例副本。

但是,如果您以一种聪明的方式对其进行编码,则可以拥有一个不变的结构,其中任何操作都可以返回该问题的更新版本(但仍然是不变的)(类似于String.replace-它不会替换字符串,只是为您提供了一个新的字符串) )。幼稚的实现方式是使“不变”结构在任何修改后都复制并创建一个新的结构,在具有可变的结构的情况下将其缩减为第二种解决方案,而所有这些开销,但是您可以在更大的范围内完成有效的方法。


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()返回aconst std::string&具有通过引用返回并防止访问mutator的作用,这意味着在该上下文中不需要不变的类。



1

不可变类的一个尚未被调用的功能:存储对深不可更改类对象的引用是一种存储其中包含的所有状态的有效方法。假设我有一个可变对象,该对象使用深度不变的对象来保存价值50K的状态信息。进一步,假设我希望在25种情况下对原始(可变)对象进行“复制”(例如,对于“撤消”缓冲区);状态可以在复制操作之间更改,但通常不会更改。制作可变对象的“副本”仅需要复制对其不可变状态的引用,因此20个副本仅相当于20个引用。相反,如果将状态保存在价值5万的可变对象中,则25个复制操作中的每一个都必须生成自己的价值5万的数据副本。拥有全部25个副本将需要保留一份价值不菲的大部分重复数据。即使第一个复制操作将产生永远不会改变的数据副本,而其他24个操作在理论上也可以简单地引用到这一点,但是在大多数实现中,第二个对象将无法获得第二个对象的副本。知道不可变副本已存在的信息(*)。

(*)一种有时有用的模式是使可变对象具有两个保持状态的字段-一个以可变形式保存而另一个以不可变形式保存。可以将对象复制为可变或不可变的对象,并从一个或另一个参考集开始生活。一旦对象想要更改其状态,它就会将不可变引用复制到可变引用(如果尚未完成),并使不可变引用无效。当对象被复制为不可变对象时,如果未设置其不可变引用,则将创建一个不可变副本,并且不可变引用指向该对象。与“完整的写入时复制”相比,此方法将需要更多的复制操作(例如,要求复制自上次复制以来已发生突变的对象将需要复制操作,


1

为什么是不可变类?

实例化对象后,其状态就无法更改。这也使其线程安全。

例子 :

显然是String,Integer和BigDecimal等。一旦创建了这些值,就无法在生命周期中更改它们。

用例:一旦使用其配置值创建了数据库连接对象,您可能无需更改其状态即可使用不可变类


0

来自有效Java;不可变类只是其实例无法修改的类。每个实例中包含的所有信息在创建时都会提供,并且在对象的生命周期内是固定的。Java平台库包含许多不可变的类,包括String,装箱的原始类以及BigInteger和BigDecimal。这样做的原因有很多:不可变类比可变类更易于设计,实现和使用。它们不太容易出错,并且更安全。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.