可变类还是不可变类?

Posted

技术标签:

【中文标题】可变类还是不可变类?【英文标题】:Mutable or immutable class? 【发布时间】:2010-11-20 01:28:36 【问题描述】:

我在一些设计书籍中读到不可变类提高了可伸缩性,以及尽可能编写不可变类的良好实践。但我认为这样不可变的类会增加对象的增殖。那么使用不可变类还是更好地使用静态类(所有方法都是静态的类)以提高可伸缩性是更好的选择吗?

【问题讨论】:

【参考方案1】:

immutable 类的主要好处是您可以公开不可变的内部数据成员,因为调用者无法修改它们。这是一个很大的问题,比如java.util.Date。它是可变的,因此您不能直接从方法中返回它。这意味着你最终会做各种defensive copying。这会增加对象的增殖。

另一个主要好处是,根据定义,不可变对象没有synchronization 问题。这就是scalability 问题出现的地方。编写multithreaded 代码很难。不可变对象是(大部分)规避问题的好方法。

至于“静态类”,根据您的评论,我认为它是指带有factory methods 的类,通常是这样描述的。这是一个不相关的模式。可变类和不可变类都可以具有公共构造函数或具有静态工厂方法的私有构造函数。这对类的(不)可变性没有影响,因为可变类的状态可以在创建后更改,而不可变类的状态在实例化后无法更改。

不过,静态工厂方法还有其他好处。这个想法是封装对象的创建。

【讨论】:

静态类是你从不实例化的类,但使用它提供的方法(例如 java 中的数组)。不过,我看不出它们如何取代不可变的类...... 可能这里的“静态类”只是“不可变类”的同义词。没有其他意义。 使用“静态类”我的意思是说一个包含所有带有私有构造函数的静态方法的类(因此用户不能创建同一类的实例)而没有任何静态成员。很抱歉造成混乱。【参考方案2】:

不可变类确实会促进对象增殖,但如果您想要安全,可变对象将促进更多对象增殖,因为您必须返回副本而不是原始对象,以防止用户更改您返回的对象.

至于使用所有静态方法的类,在大多数可以使用不变性的情况下,这并不是一个真正的选择。以 RPG 为例:

public class Weapon

    final private int attackBonus;
    final private int accuracyBonus;
    final private int range;

    public Weapon(int attackBonus, int accuracyBonus, int range)
    
        this.attackBonus = attackBonus;
        this.accuracyBonus = accuracyBonus;
        this.range = range;
    

    public int getAttackBonus()  return this.attackBonus; 
    public int getAccuracyBonus()  return this.accuracyBonus; 
    public int getRange()  return this.range; 

您将如何使用仅包含静态方法的类来实现这一点?

【讨论】:

Java SE 16 让创建不可变类变得异常容易。检查***.com/a/65976915/10819573【参考方案3】:

正如 cletus 所说,不可变类简化了同步方法中的类设计和处理。

它们还简化了集合中的处理,即使在单线程应用程序中也是如此。一个不可变的类永远不会改变,所以键和哈希码不会改变,所以你不会搞砸你的集合。

但是您应该牢记您正在建模的事物的生命周期以及构造函数的“权重”。如果你需要改变事物,不可变对象会变得更复杂。您必须替换它们,而不是修改它们。不可怕,但值得考虑。如果构造函数花费大量时间,那也是一个因素。

【讨论】:

【参考方案4】:

需要考虑的一点:如果您打算将类的实例用作 HashMap 中的键,或者如果您要将它们放入 HashSet,则将它们设为不可变会更安全。

HashMap 和 HashSet 依靠这样一个事实,即只要对象在映射或集合中,对象的哈希码就会保持不变。如果您使用对象作为 HashMap 中的键,或者将其放入 HashSet,然后更改对象的状态以使 hashCode() 返回不同的值,那么您会混淆 HashMap 或 HashSet 并且你会得到奇怪的东西;例如,当您迭代地图或设置对象存在时,但当您尝试获取它时,就好像它不存在一样。

这是由于 HashMap 和 HashSet 内部的工作方式 - 它们通过哈希码组织对象。

This article 由 Java 并发专家 Brian Goetz 提供,很好地概述了不可变对象的优缺点。

【讨论】:

【参考方案5】:

不变性通常用于实现可伸缩性,因为不变性是 Java 并发编程的促成因素之一。因此,正如您所指出的,“不可变”解决方案中可能有更多对象,但这可能是提高并发性的必要步骤。

不变性的另一个同样重要的用途是使用设计意图;制作不可变类的人希望您将可变状态放在其他地方。如果你开始改变那个类的实例,你可能会破坏设计的初衷——谁知道后果可能是什么。

【讨论】:

【参考方案6】:

以字符串对象为例。有些语言或类库提供可变字符串,有些则不提供。

使用不可变字符串的系统可以进行某些优化,而使用可变字符串的系统则不能。例如,您可以确保任何唯一字符串只有一个副本。由于对象“开销”的大小通常远小于任何重要字符串的大小,因此这可能会节省大量内存。还有其他潜在的空间节省,例如实习子字符串。

除了潜在的内存节省之外,不可变对象还可以通过减少争用来提高可伸缩性。如果您有大量线程访问相同的数据,那么不可变对象不需要复杂的同步过程来进行安全访问。

【讨论】:

【参考方案7】:

关于这个主题的再考虑一点。使用不可变对象允许您缓存它们而不是每次都重新创建它们(即字符串),这对您的应用程序性能有很大帮助。

【讨论】:

【参考方案8】:

我认为,如果你想在不同的变量之间共享同一个对象,它需要是不可变的。

例如:

String A = "abc";
String B = "abc";

Java 中的字符串对象是不可变的。现在 A & B 都指向同一个“abc”字符串。 现在

A = A + "123";
System.out.println(B);

它应该输出:

abc

由于 String 是不可变的,A 将简单地指向新的“abc123”字符串对象,而不是修改之前的字符串对象。

【讨论】:

-1。 A = A + "123" 将简单地构建一个新字符串 (abc123) 并将其分配给变量 A。字符串是不可变的,因为您无法将 123 字符串转换为 abc - String 对象没有任何分配选项。但是,从另外两个对象中构建一个新的 String 对象是完全可以的。 好吧,一个新的字符串被构建了,A 将指向新的“abc123”。这里的焦点区域是指向新对象的“点”。我没有说“abc123”是修改现有“abc”或“123”的结果。

以上是关于可变类还是不可变类?的主要内容,如果未能解决你的问题,请参考以下文章

Scala中string对象是可变还是不可变?加入要创建一个可以修改得字符串,应该是那个类

Java 不可变类

JAVA不可变类(immutable)机制与String的不可变性

不可变类

不可变类

不可变类