Java中的不变性

如果我的类将是一个不可变的,它必须是final ,没有任何修改其状态的方法,所有属性必须是私有的。 但是为什么它应该将属性声明为final (例如, private final int a )?

编辑

如果类具有对不可变的对象的引用,那么该类仍然是不可变的吗?

来自Jeremy Manson的博客文章Immutability in Java :

现在,按照通常的说法,不变性意味着“不会改变”。 不可变性并不意味着Java中的“不会改变”。 它意味着“从最终字段可以传递到达,自设置最终字段以来没有改变,并且对包含最终字段的对象的引用没有逃脱构造函数”。

Manson共同撰写了Java Memory Model规范,因此应该知道他在说什么。

Brian Goetz的Java Concurrency in Practice一书中的一个例子:

 public Holder holder; public void initialize() { holder = new Holder(42); } publc class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) { throw new AssertionError(”This statement is false”); } } } // Thread 1: // Thread 2: initialize(); if (holder != null) holder.assertSanity(); 

上面的线程2可以抛出AssertionError 。 为避免此类可见性问题, Holder类的字段n应已声明为final

编辑:考虑一系列步骤:

  1. Holder类型的新对象分配内存
  2. 初始化新对象的字段n
  3. 执行构造函数
  4. 将新对象的引用分配给holder引用

这是“理智”的事件序列,JMM(Java内存模型)保证这是调用initialize()的线程所看到的。 但是,由于没有同步,JMM不保证调用holder.assertSanity()的第二个线程将看到什么序列。 特别是,如果第二个线程看到序列,则抛出AssertionError

  1. holder是非null的,因此它可以调用assertSanity方法
  2. n的第一次读取结果为0,即int字段的默认值
  3. n的第二次读数结果为42

简而言之,当一个对象的构造函数在一个线程中返回时,另一个线程可能看不到该对象的所有字段初始化。 为了保证第二个线程看到正确初始化的字段,您应该使它们成为final字段,或者执行互斥锁解锁(即initialize()方法应该synchronized )或执行volatile写入(即holder应该是volatile )。

如果将私有成员变量标记为final ,则需要使用编译器和运行时引擎来确保其值永远不会被类本身或其子类修改。

最后一类是不能被子类化的类。 类定义的final关键字不一定使该类不可变。 所以你的类不必是最终的,因为它的实例是不可变的(你的问题暗示你正在使类最终,注意final可以用来修改不仅仅是类)。 但是,如果您创建一个类final,该类不能被子类化,那么您可以保证不会创建任何可变子类。

不变性只是意味着不能改变类中的值。 所以私人领域,没有非私人制定者应该这样做。 一个常见的问题是,如果您的类具有Collection,则必须返回某种不可修改的集合,否则可以更改集合中的值,即使没有人可以更改对集合的引用 。 这回答了你问题的第二部分。 如果在你的类上声明一个final myCollection ,你就不能改变对myCollection的引用,但你仍然可以在类之外修改集合,如果你有一个getter(很好)并且不返回一个不可修改的集合。 请参阅http://download.oracle.com/javase/6/docs/api/java/util/Collections.html#unmodifiableCollection(java.util.Collection )

关于不变性,请记住,一旦最初设置了值,目标就是无法更改类的实例。 使用final关键字可以提供帮助,但本身并不足以拥有不可变的定义。

并非严格要求不可变类的属性是最终的。 如果不可变类本身是最终的,并且在构造之后注意不要更改属性,那么将这些属性设置为final是合法的。

但是没有理由让它们不是最终的。 使它们成为最终版本允许编译器检查您的代码并防止意外修改。 它还记录了这样一个事实:您不希望这些属性被多次写入(在创建时)。