可变类还是不可变类?

9

我曾经在一些设计书籍中读到,不可变类可以提高可伸缩性,并且在任何可能的情况下编写不可变类是良好的实践。但是我认为,不可变类会增加对象数量。那么是编写不可变类好还是使用静态类(所有方法都为静态的类)来提高可伸缩性更好呢?

8个回答

16
不可变类的主要优点在于,您可以公开内部数据成员,这些成员是不可变的,因为调用者无法修改它们。这对于例如java.util.Date等可变对象来说是一个巨大的问题。它是可变的,因此无法直接从方法中返回。这意味着您最终会做出各种防御性拷贝。这会增加对象数量。
另一个主要优点是,不可变对象没有同步问题,这是根据定义的。这就是可扩展性问题出现的地方。编写多线程代码很难。不可变对象是(大多数情况下)规避问题的好方法。
至于“静态类”,根据您的评论,我认为您指的是具有工厂方法的类,这通常是如何描述的。那是一种无关的模式。可变和不可变类都可以具有公共构造函数或私有构造函数以及静态工厂方法。这对类的(不)可变性没有影响,因为可变类是其状态可以在创建后更改的类,而不可变类的状态在实例化后不能更改。
然而,静态工厂方法可能具有其他好处。想法是封装对象创建。

1
静态类是一种你永远不会实例化的类,但可以使用其提供的方法(例如Java中的数组)。然而,我并不认为它们能够取代不可变类... - Zed
这里的“静态类”可能只是“不可变类”的同义词。其他解释都说不通。 - CPerkins
使用“静态类”这个术语,我是指一个包含所有静态方法和私有构造函数的类(因此用户无法创建同一类的实例),而没有任何静态成员。对于造成的混淆,我感到抱歉。 - Silent Warrior

5

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

至于使用所有静态方法的类,在大多数可以使用不可变性的情况下,这并不是一个真正的选择。以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; }
}

如果只有包含静态方法的类,那么你要如何实现它呢?


1
Java SE 16 使得创建不可变类变得非常容易。请访问 https://stackoverflow.com/a/65976915/10819573 了解更多信息。 - Arvind Kumar Avinash

1

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

它们还简化了集合的处理,即使在单线程应用程序中也是如此。不可变类永远不会改变,因此键和哈希码不会改变,因此您不会破坏您的集合。

但是,您应该牢记您正在建模的事物的生命周期和构造函数的“重量”。如果您需要更改该事物,则不可变对象变得更加复杂。您必须替换它们而不是修改它们。这并不可怕,但值得考虑。如果构造函数需要较长时间,那么这也是一个因素。


1

需要考虑的一件事情是:如果你打算将一个类的实例用作HashMap中的键,或者将它们放入HashSet中,最好使它们不可变。

HashMap和HashSet依赖于对象的哈希码在对象在映射或集合中时保持不变。如果你将一个对象用作HashMap中的键,或者将其放入HashSet中,然后更改对象的状态以使hashCode()返回不同的值,则会混淆HashMap或HashSet,你会得到奇怪的结果;例如,当你迭代映射或集合时,对象存在,但当你尝试获取它时,就好像它不存在一样。

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

Java并发专家Brian Goetz的这篇文章概述了不可变对象的优缺点。


0

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

例如:

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对象是完全可以的。 - cthulhu
一个新的字符串被创建了,A将指向NEW的"abc123"。这里的重点是A“指向”NEW对象。我没有说"abc123"是现有的"abc"或"123"的修改结果。 - janetsmith

0

不变性通常用于实现可扩展性,因为在Java并发编程中,不变性是其中一个实现方式。因此,正如您所指出的那样,“不可变”解决方案中可能会有更多的对象,但这可能是提高并发性能的必要步骤。

同样重要的是,不变性的另一个用途是消耗设计意图;制作不可变类的人打算让您将可变状态放在其他地方。如果您开始改变该类的实例,则可能会破坏设计的原始意图 - 谁知道后果会是什么。


0
考虑字符串对象,举个例子。有些语言或类库提供可变字符串,有些则不提供。
使用不可变字符串的系统可以进行某些优化,而使用可变字符串的系统则不能。例如,您可以确保任何唯一字符串只有一个副本。由于对象“开销”的大小通常比任何非平凡字符串的大小要小得多,因此这是潜在的大量内存节省。还有其他潜在的空间节省,如interning substrings。
除了潜在的内存节省外,不可变对象通过减少争用来提高可扩展性。如果有大量线程访问相同的数据,则不可变对象不需要复杂的同步过程来进行安全访问。

0
关于这个主题还有一个要考虑的问题。使用不可变对象可以让您缓存它们,而不是每次重新创建它们(例如字符串),这对应用程序性能有很大帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接