有效不可变对象是否有意义?

11
在书籍Java Concurrency In Practice中,它解释了“有效不可变”对象在并发方面相对于可变对象的优势。但它没有解释“有效不可变”对象相对于真正不可变对象的优势。
我不明白:你不能总是在你决定安全发布“有效不可变”对象的时刻构建一个真正不可变的对象吗?(而不是进行“安全发布”,你将构建一个真正不可变的对象,就这样)
当我设计类时,我看不到哪些情况下我不能总是在我决定“安全发布”的时刻构建一个真正不可变的对象(如果需要使用委托等来构建其他包装对象,则它们本身当然是真正不可变的)。
那么,“有效不可变”对象及其“安全发布”只是一种糟糕的设计或API不良的情况吗?
在哪些情况下你被迫使用“有效不可变”对象并被迫安全地发布它,而你不能构建一个更优秀的真正不可变的对象?
4个回答

10

在某些情况下,它们是有意义的。一个简单的例子是当您想要一些属性被惰性生成并缓存,以便在没有访问时避免生成它的开销。 String 是一个有效不可变类的例子,它使用了这种技术(使用其哈希码)。


4

对于循环不可变对象:

class Foo
{
    final Object param;
    final Foo other;

    Foo(Object param, Foo other)
    {
        this.param = param;
        this.other = other;
    }

    // create a pair of Foo's, A=this, B=other
    Foo(Object paramA, Object paramB)
    {
        this.param = paramA;
        this.other = new Foo(paramB, this);
    }

    Foo getOther(){ return other; }
}



// usage
Foo fooA = new Foo(paramA, paramB);
Foo fooB = fooA.getOther();
// publish fooA/fooB (unsafely)

一个问题是,由于fooAthis在构造函数中泄漏,fooA是否仍然是线程安全的不可变对象?也就是说,如果另一个线程读取fooB.getOther().param,它能否保证看到paramA?答案是肯定的,因为在“freeze”操作之前,this没有泄漏到另一个线程;我们可以建立规范所需的hb/dc/mc顺序来证明paramA是读取的唯一可见值。
回到您最初的问题。实际上,除了纯技术之外,总是存在其他限制。在构造函数中初始化所有内容并不一定是设计的最佳选择,考虑到所有的工程、运营、政治和其他人类原因。
曾经想过为什么我们被灌输认为这是一个伟大的至高无上的想法吗?
更深层次的问题是,Java缺乏一种通用的比volatile更便宜的安全发布机制。Java只有对于final字段才有这个机制;出于某种原因,这个机制在其他情况下不可用。
现在,final具有两个独立的含义:第一,final字段必须只能被分配一次;第二,安全发布的内存语义。这两个含义并不相互关联。将它们绑定在一起非常令人困惑。当人们需要第二个含义时,他们被迫接受第一个含义。当第一个含义在设计中非常不方便时,人们会想知道自己做错了什么——却没有意识到是Java做错了。
将两个含义捆绑在一个final中使其变得更加好用,因此我们显然有更多的理由和动机使用final。更阴险的故事实际上是我们被迫使用它,因为我们没有更灵活的选择。

对于这个很好的例子和解释,我点赞。另外,我没有提到“final”,但确实是我们必须使用的...关于两个含义:更令人困惑的是,final也可以应用于方法,但我离题了;)我喜欢构造函数的例子,尽管人们也可以使用工厂和/或流畅接口来完成它。 - NoozNooz42

1

使用有效的不可变对象可以避免创建大量的类。您可以构建一个有效的不可变类,而不是制作[可变构建器]/[不可变对象]类对。我通常定义一个不可变接口和一个实现该接口的可变类。通过其可变类方法配置对象,然后通过其不可变接口发布对象。只要您的库的客户端按照接口编程,他们的对象在其发布的生命周期内保持不可变。


这种方法的任何示例可用吗? - user77115

0
假设有一个不可变类Foo,它有五个属性,分别命名为AlphaBeta等,希望提供WithAlphaWithBeta等方法,这些方法将返回一个与原始实例相同但具有特定属性更改的实例。如果该类真正且深度不可变,则这些方法必须采取以下形式:
Foo WithAlpha(string newAlpha)
{ 
  return new Foo(newAlpha, Beta, Gamma, Delta, Epsilon);
}
Foo WithBeta(string newBeta) { return new Foo(Alpha, NewBeta, Gamma, Delta, Epsilon); }
这是一种“不要重复自己”(DRY)原则的严重违反。此外,向类添加新属性需要将其添加到每个方法中。
另一方面,如果每个Foo都包含一个内部的FooGuts,其中包括一个副本构造函数,那么可以做如下操作:
带有Alpha的Foo(string newAlpha)
{
  FooGuts newGuts = new FooGuts(Guts); // Guts是一个私有或受保护的字段
  newGuts.Alpha = newAlpha;
  return new Foo(newGuts); // 私有或受保护的构造函数
}
每个方法的代码行数增加了,但是这些方法不再需要引用任何它们不“感兴趣”的属性。请注意,如果使用FooGuts调用其构造函数的Foo存在外部引用,则该Foo可能不是不可变的,但是只有受信任的代码才能访问其构造函数,并且在构造后不会维护任何此类引用。

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