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

95

我无法理解何时需要不可变类的情况。
您是否有遇到过此类要求?或者可以给我们任何一个需要使用此模式的实际示例吗?


14
我很惊讶没有人指出这是重复的。看起来你们只是想轻松地积累分数... :) (1) https://dev59.com/MEjSa4cB1Zd3GeqPJeq0 (2) https://dev59.com/mHA75IYBdhLWcg3wrrJS (3) https://dev59.com/A3RB5IYBdhLWcg3wAjNH - Javid Jamae
20个回答

3

通过不可变性,您可以确保底层不可变对象的行为/状态不会改变,从而获得执行其他操作的额外优势:

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

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

  • 可以轻松进行调试(因为运行历史记录将不再是一个问题)


3

不可变对象是指一旦初始化,其状态不会改变的实例。使用这样的对象是根据需求而定的。

不可变类适用于缓存目的,并且它是线程安全的。


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 关键字的,而是关于不可变类的。您可以使用适当的访问限制在没有 final 关键字的情况下拥有一个不可变类。 - Mark Peters

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()返回一个const std::string&的效果是通过引用返回并防止访问mutators,这意味着在该上下文中不需要不可变类。

1
不可变类的一个特点尚未被提及:存储对深度不可变类对象的引用是一种有效的存储其中包含的所有状态信息的方法。假设我有一个可变对象,它使用深度不可变对象来保存50K的状态信息。进一步假设,我希望在25个场合下制作原始(可变)对象的“副本”(例如,“撤消”缓冲区);状态可能会在复制操作之间发生变化,但通常不会。制作可变对象的“副本”只需要复制对其不可变状态的引用,因此20个副本只会简单地相当于20个引用。相比之下,如果状态保存在50K的可变对象中,每个25个复制操作都必须产生自己的50K数据副本;持有所有25个副本将需要持有超过1兆字节的大部分重复数据。即使第一个复制操作将产生永远不会改变的数据副本,并且其他24个操作理论上可以简单地参考该副本,但在大多数实现中,请求信息副本的第二个对象无法知道已经存在不可变副本(*)。
(*) 有时候,可变对象具有两个字段来保存它们的状态--一个是可变形式,另一个是不可变形式。对象可以作为可变或不可变的副本进行复制,并且会开始使用其中一个引用。当对象想要改变其状态时,它将不可变引用复制到可变引用中(如果尚未完成),并使不可变引用无效。当对象作为不可变的副本进行复制时,如果其不可变引用未设置,则将创建一个不可变副本,并将不可变引用指向该副本。这种方法将需要比“完全写时复制”更多的复制操作(例如,即使原始对象再也不被修改,请求复制已经发生变异的对象仍需要进行复制操作),但它避免了FFCOW所涉及的线程复杂性。

1

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

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

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

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

然而,如果你以巧妙的方式编写代码,你可以拥有一个不可变的结构,在这个结构中,任何操作都会返回问题的更新版本(但仍然是不可变的),类似于String.replace——它不会替换字符串,只是给你一个新的字符串。实现这一点的天真方法是在任何修改时只需复制并创建一个新的“不可变”结构,将其降级为第二种解决方案,即具有所有开销的可变结构,但你可以以更有效的方式实现它。


1

1
为什么要使用不可变类?
一旦对象被实例化,它的状态在生命周期内无法更改。这也使其线程安全。
例如:显然,String、Integer和BigDecimal等。一旦创建这些值,就无法在生命周期内更改。
用例:一旦使用其配置值创建了数据库连接对象,您可能不需要更改其状态,可以使用不可变类。

0

来自于《Effective Java》; 不可变类仅仅是指其实例无法被修改的类。每个实例所包含的所有信息都是在创建时提供的,并且在对象的生命周期内都是固定的。Java平台库中包含许多不可变类,包括String、包装的基元类型类和BigInteger以及BigDecimal。这其中有很多好处:不可变类比可变类更容易设计、实现和使用。它们更不容易出错,更加安全。


0
一个不可变类对于缓存是有益的,因为你不必担心值的更改。另一个不可变类的好处是它本质上是线程安全的,因此在多线程环境下不必担心线程安全性。

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