Java中的不可变性

3
在《Effective Java》中,Bloch建议使对象不可变时将所有字段设为final。这样做是必要的吗?只是不提供访问器方法就可以使其不可变吗?例如:
class A {
      private int x;
      A (int x) {
          this.x = x;
      }
}

上述类是不可变的,即使我没有将 x 声明为 final,对吗?我有什么遗漏吗?

访问器方法(例如 getX())是可以的,问题在于“变异器”方法(例如 setX())。 - Grundlefleck
@user2434,即使没有声明为final的所有字段,您也可以拥有一个不可变的类。但这意味着非final字段的不可变性不会影响对象的不可变性。实际上,String就是这样一个类的例子。请参见https://dev59.com/pGgu5IYBdhLWcg3wfHI_。 - Inquisitive
@Grundlefleck:即使使用final修饰符,get()方法也可能存在问题。例如,如果你有一个int[]数组用于存储集合中的元素,那么对该int进行get()操作会允许用户修改它。因此,在使用get()方法时也需要小心谨慎。 - user1111929
7个回答

6
除了@Bozho所说的,将一个字段声明为final意味着可以在没有任何同步的情况下安全地访问它。
相比之下,如果该字段不是final,则存在小概率风险,即如果在没有适当同步的情况下访问该字段,则另一个线程可能会看到该字段的异常值。即使在对象构造之后没有改变字段的值也可能发生这种情况!

4

它并非完全不可变,因为您可以更改值。下一步将是团队中的其他人将新值分配给字段。 final 表示不可变性的意图。


除了反射之外,它是“完全”不可变的。它更容易被程序员错误地设置为不可变的事实是有效的,但这并不改变他问题的答案实际上是肯定的这一事实。 - Robin

3

'Effectively Immutable'类是由Java Concurrency In Practice 这本书定义的术语。

这意味着只要实例的引用“安全地发布”,它们就是不可变的。安全地发布引用需要使用同步,以便Java内存模型(JMM)可以保证调用者将完整写入的字段的值。例如,如果该字段不是final的,并且构造了一个实例并传递给另一个线程,则其他线程可能会看到未定义状态的字段(例如null(如果它是对象引用),或64位long字段的一半)。

如果实例仅在单个线程中使用,则区别无关紧要。这是因为JMM使用“线程内as-if-serial”语义。因此,在构造函数中分配字段将始终在字段被读取之前发生。

如果该字段是final的,则JMM将保证调用者将看到正确的值,无论如何发布引用。因此,如果您想将实例传递给其他线程而不使用同步形式,则final具有优势。


2
你可以这样做,但是如果你在声明时使用了final,编译器会帮助你。一旦你尝试给成员变量赋新值,编译器就会抛出错误。

假设我将 'x' 声明为 final,我可以执行 A a = new A(5),然后在下一行写入 a = new A(7)。编译器不会警告我任何东西。我有什么遗漏吗? - user2434
1
@user2434 - 然后您创建了两个不可变的A实例,并将它们都分配给同一个变量。第一个现在将被垃圾回收。这与A的不可变性无关。 - Robin

2

目前来看,是的,这个类是不可变的。当然,忽略反射。

然而,正如@Bozho所说,只需要有人添加一个方法就可以改变这一点。

将x设置为final可以提供额外的安全性,并使您的意图更明确。


至少有人指出他所拥有的实际上是不可变的。指出它更容易被改变为不可变的,或者它不够线程安全都是有效的观点,但是否它是不可变的这个实际问题仍然需要回答。 - Robin

2
您可以使用setAccessible设置私有字段。这就是Spring、Hibernate等框架的操作方式。如果A也可以被子类化,那么问题就在于A的所有实例是否都是不可变的。
使不可变性明确化的主要好处是使编码者的意图清晰明了。final没有setter,因此读取代码时不需要查找它。我通常也在类注释中说明不可变性的意图。
(假设您知道不可变性的一般好处,因为您只问了机制方面的问题。)

0
除了可以添加更改非 final 字段值的代码之外,JVM 对 final 和非 final 字段的处理方式是不同的。Java 内存模型 中有一节关于这个主题的内容(相当严肃的阅读)。

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