不可变对象和有效不可变对象有何区别?

21

这是来自《Java并发编程实践》的一句话。

共享的只读对象包括不可变对象和有效不可变对象。

不可变对象和有效不可变对象有什么区别?


请参阅Do effectively immutable objects make sense,该文章还提到了您所提到的书籍。 - Bobulous
我已经实现了一个完整的Java不可变性检测器,并将其作为答案提供给了另一个相关问题。它完全封装了“有效的不可变性”。https://dev59.com/IloU5IYBdhLWcg3wzZLw#75043881 - chaotic3quilibrium
4个回答

17

一个类不可扩展,它的所有字段都是final且本身不可变,那么该类的实例是不可变的。

如果一个类的字段由于其方法的细节而不能被改变,那么该类的实例在实际上也是不可变的。例如:

final class C {
  final boolean canChange;
  private int x;
  C(boolean canChange) { this.canChange = canChange; }
  public void setX(int newX) {
    if (canChange) {
      this.x = newX;
    } else {
      throw new IllegalStateException();
    }
  }
}

C的一些实例是有效不可变的,而另一些则不是。

另一个例子是零长度数组。尽管它们所在的类不能被证明为不可变类,但它们本身实际上是不可变的,因为它们没有任何可以改变的元素。


Joe-E使用验证器来证明某些类仅允许使用不可变实例。任何标记了Immutable接口的内容都会被检查,像String这样的特定类(实际上是不可变的,因为其char[]未泄露)被视为是不可变的。

Joe-E:Java的安全子集称:

由Joe-E库定义的Immutable接口在语言中得到了特殊处理:Joe-E验证器检查每个实现此接口的对象是否是(深度)不可变的,并在无法自动验证时引发编译时错误。


1
一个类的实例由于其方法的细节而无法改变其字段,因此是有效不可变的,这是不准确的。_有效不可变_可以是任何对象,只要在安全发布后(例如通过volatile引用等)不被改变即可。 - Op De Cirkel
@OpDeCirkel,“immutable”上的“-able”后缀使其意思为“不可改变”,而不仅仅是“未发生突变”。如果您有证据表明普通含义并非Goetz等人的意思,请添加答案,我会点赞。 - Mike Samuel
1
JCP 3.5.4:那些在技术上不是不可变的对象,但其状态在发布后不会被修改的对象称为有效不可变对象。- 此定义包括比没有改变其状态的方法更广泛的集合。只要有约定,在安全发布后不应该对对象进行修改就足以成为有效不可变对象。 - Op De Cirkel
1
此外,定义(和问题)讨论的是有效不可变的“对象”,而不是类。在我之前提到的同一部分 - JCP 3.5.4 - 使用java.util.Date作为例子来讨论有效不可变性的上下文(尽管它有可以改变其状态的方法)。 - Op De Cirkel
将_effective immutability_概念应用于类的另一个问题是,当类具有包私有方法来改变其状态和不改变状态的公共方法时,这将变得更加复杂。这是否是_effectively immutable_呢? - Op De Cirkel
@OpDeCirkel,感谢您提供的引用。如果您将其写成答案,我会点赞并建议将其接受为被采纳的答案。 - Mike Samuel

13

通过一番搜索和找到这篇文章,我对Effectively Immutable Object有了一些理解。它是一个包含可变字段的对象,但不会让任何东西修改这些字段,因为它从未给你一个引用。例如,假设你创建了一个带有ArrayList的类。ArrayList是可变的,但是如果你的类总是返回ArrayList的副本,并且你的类中其他所有内容都是不可变的,那么你的类就成为了实际上的不可变类:一个类的实例状态是不可变的。

博客文章提供了一个Effectively Immutable Class的示例:

import java.awt.*;

public class Line {

   private final Point start;
   private final Point end;

   public Line(final Point start, final Point end) {
       this.start = new Point(start);
       this.end = new Point(end);
   }

   public void draw() {
       //...
   }

   public Point getStart() {
       return new Point(start);
   }

   public Point getEnd() {
       return new Point(end);
   }
}

Point对象是可变的,但这没问题,因为这个类不直接将引用传给任何人的Point实例。相反,它返回一个新的具有同样值的实例。这样,没有人可以改变Line类的状态。这使得Line类有效地成为不可变的。

那么这与真正的不可变类有什么区别呢?真正的不可变类具有不可变的字段。假设Line是真正的不可变类,那么我们也需要假设Point是不可变的。在这些假设的基础上,getStart()方法可以被写成这样:

public Point getStart() {
   return start;
}

术语可能有所不同,但我认为“有效的不可变性”是对象实例的特征,而不是类型。如果没有执行路径可以通过该引用暴露给可能会改变它的代码,那么该实例将是有效的不可变的,即使它的类不是。我认为一个仅保存值和对不可变类型或有效不可变实例的引用,并且不公开任何改变其自身形状的方法的对象是不可变的。 - supercat
@supercat 从短语的英文含义来看,这是有意义的。 - Daniel Kaplan

3
看一下这个答案:
“effectively immutable”(有效不可变)和“immutable”(不可变)之间的区别在于,在前一种情况下,您仍然需要以安全的方式发布对象。对于真正不可变的对象,则不需要这样做。因此,真正不可变的对象更受欢迎,因为它们更容易发布。我上面提到的原因解释了为什么您可能更喜欢不同步地发布。 https://dev59.com/G2sz5IYBdhLWcg3wcnhb#7887675

2

不变对象完全封装了它们的内部状态,并且在构造后不允许修改该状态(可能使用final等方式),因此它们可以安全地在多个线程之间共享,因为从共享对象读取不会对多个线程产生伤害。

有效的不可变对象可能在被多个线程共享之前更改其状态,但是在它们被“发布”(即给出多个引用以供几个线程使用)后,它们会保护自己免受修改。

不变对象阻止您使用有用的软件工程实践,例如延迟初始化,因为为了懒惰地初始化属性或字段,它们必须是可变的,从而违反了它们无忧并发共享属性。有效的不可变对象通过仔细知道何时可以安全地修改其内部状态以及何时禁止修改来放松这些约束,从而实现了最佳的两个方面的方法。


1
是的,String 是不可变的,并且防止您直接修改其内部状态,但在计算其哈希码时它使用了惰性初始化。因此从技术上讲,String 是有效不可变的。 - chubbsondubs

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