[英]java : Question regarding immutable and final
我正在阅读《有效Java》这本书。
在一个项目最小化可变性中,Joshua Bloch 谈到了使 class 不可变。
不要提供任何修改对象的 state 的方法——这很好。
确保 class 不能扩展。 - 我们真的需要这样做吗?
使所有字段最终确定——我们真的需要这样做吗?
例如,假设我有一个不可变的 class,
class A{
private int a;
public A(int a){
this.a =a ;
}
public int getA(){
return a;
}
}
从 A 延伸的 class 如何损害 A 的不变性?
像这样:
public class B extends A {
private int b;
public B() {
super(0);
}
@Override
public int getA() {
return b++;
}
}
从技术上讲,您没有修改从A
继承的字段,但是在不可变的 object 中,重复调用相同的 getter 肯定会产生相同的数字,但这里不是这种情况。
当然,如果您坚持规则 #1,则不允许创建此覆盖。 但是,您不能确定其他人会遵守该规则。 如果您的某个方法将A
作为参数并对其调用getA()
,则其他人可能会像上面一样创建 class B
并将其实例传递给您的方法; 然后,您的方法将在不知不觉中修改 object。
Liskov 替换原则说子类可以在超级 class 所在的任何地方使用。 从客户的角度来看,孩子是父母。
因此,如果您覆盖子项中的方法并使其可变,则您违反了与期望它是不可变的父项的任何客户的合同。
如果您声明一个字段final
,除了让它成为编译时错误来尝试修改该字段或让它保持未初始化之外,还有更多的事情要做。
在多线程代码中,如果您与数据竞争共享 class A
的实例(即,没有任何同步,即通过将其存储在全局可用位置,例如 static 字段),则某些线程可能会看到getA()
的值改变了!
在构造函数完成后, Final
字段(由JVM 规范)保证其值对所有线程可见,即使没有同步也是如此。
考虑这两个类:
final class A {
private final int x;
A(int x) { this.x = x; }
public getX() { return x; }
}
final class B {
private int x;
B(int x) { this.x = x; }
public getX() { return x; }
}
A
和B
都是不可变的,因为您不能在初始化后修改字段x
的值(让我们忘记反射)。 唯一的区别是字段x
在 A 中被标记为final
。您很快就会意识到这个微小差异的巨大影响。
现在考虑以下代码:
class Main {
static A a = null;
static B b = null;
public static void main(String[] args) {
new Thread(new Runnable() { void run() { try {
while (a == null) Thread.sleep(50);
System.out.println(a.getX()); } catch (Throwable t) {}
}}).start()
new Thread(new Runnable() { void run() { try {
while (b == null) Thread.sleep(50);
System.out.println(b.getX()); } catch (Throwable t) {}
}}).start()
a = new A(1); b = new B(1);
}
}
假设两个线程在主线程设置它们后碰巧看到它们正在监视的字段不是 null(请注意,虽然这个假设可能看起来微不足道,但 JVM 不能保证。)。
在这种情况下,我们可以确定监视a
的线程将打印值1
,因为它的x
字段是 final ——因此,在构造函数完成后,可以保证看到 object 的所有线程都会看到正确的x
的值。
但是,我们无法确定其他线程会做什么。 规范只能保证它将打印0
或1
。 由于该字段不是final
,并且我们没有使用任何类型的同步( synchronized
或volatile
),线程可能会看到未初始化的字段并打印 0,另一种可能性是它实际上看到了已初始化的字段。 并打印 1。它不能打印任何其他值。
另外,可能发生的情况是,如果您继续读取并打印b
的getX()
的值,它可能会在打印0
一段时间后开始打印1
,在这种情况下,很清楚为什么不可变对象必须具有其字段final
:从第二个线程的角度来看, b
已经改变,即使它应该是不可变的,因为不提供设置器!
如果您想保证第二个线程将看到x
的正确值而不使字段final
,您可以声明保存B
volatile 实例的字段:
class Main {
// ...
volatile static B b;
// ...
}
另一种可能性是在设置和读取字段时进行同步,方法是修改 class B:
final class B {
private int x;
private synchronized setX(int x) { this.x = x; }
public synchronized getX() { return x; }
B(int x) { setX(x); }
}
或者通过修改 Main 的代码,在字段b
被读取和写入时添加同步——注意两个操作必须在同一个对象上同步!
如您所见,最优雅、最可靠和高性能的解决方案是使字段x
最终化。
最后一点,不可变的线程安全类的所有字段都不是绝对必要的 final 。 但是,这些类(线程安全、不可变、包含非最终字段)必须非常小心地设计,并且应该留给专家。
这方面的一个例子是 class java.lang.String。 它有一个private int hash;
字段,它不是最终的,用作 hashCode() 的缓存:
private int hash;
public int hashCode() {
int h = hash;
int len = count;
if (h == 0 && len > 0) {
int off = offset;
char val[] = value;
for (int i = 0; i < len; i++)
h = 31*h + val[off++];
hash = h;
}
return h;
}
如您所见, hashCode() 方法首先读取(非最终)字段hash
。 如果它是未初始化的(即如果它是0),它会重新计算它的值,并设置它。 对于计算 hash 代码并写入字段的线程,它将永远保持该值。
但是,即使在某个线程将其设置为其他值之后,其他线程仍可能会看到该字段的 0。 在这种情况下,这些其他线程会重新计算hash,得到完全相同的值,然后设置它。
在这里,证明 class 的不变性和线程安全性的理由是每个线程都将获得完全相同的 hashCode() 值,即使它被缓存在非最终字段中,因为它将重新计算并且完全相同的值将获得。
所有这些推理都非常微妙,这就是为什么建议在不可变的线程安全类上将所有字段标记为 final 的原因。
添加此答案以指向JVM 规范的确切部分,该部分提到为什么成员变量需要是最终的,以便在不可变的 class 中成为线程安全的。 这是规范中使用的示例,我认为非常清楚:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
同样,从规范:
class FinalFieldExample 具有最终 int 字段 x 和非最终 int 字段 y。 一个线程可能执行方法编写器,而另一个线程可能执行方法读取器。
因为 writer 方法在对象的构造函数完成后写入 f,所以 reader 方法将保证看到正确初始化的 fx 值:它将读取值 3。 因此,不能保证 reader 方法看到它的值 4。
如果 class 被扩展,则派生的 class 可能不是不可变的。
如果您的 class 是不可变的,那么所有字段在创建后都不会被修改。 final 关键字将强制执行这一点,并使其对未来的维护者显而易见。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.