java:关于不可变和最终的问题

发布于 2024-11-18 15:30:30 字数 389 浏览 1 评论 0原文

我正在读《Effective Java》一书。

在“最小化可变性”一文中,Joshua Bloch 谈到了使类变得不可变。

  1. 不要提供任何修改对象状态的方法 - 这很好。

  2. 确保该类无法扩展。 - 我们真的需要这样做吗?

  3. 将所有字段设为最终值 - 我们真的需要这样做吗?

例如,假设我有一个不可变的类,

class A{
private int a;

public A(int a){
    this.a =a ;
}

public int getA(){
    return a;
}
}

从 A 扩展的类如何损害 A 的不可变性?

I am reading the book Effective Java.

In an item Minimize Mutability , Joshua Bloch talks about making a class immutable.

  1. Don’t provide any methods that modify the object’s state -- this is fine.

  2. Ensure that the class can’t be extended. - Do we really need to do this?

  3. Make all fields final - Do we really need to do this?

For example let's assume I have an immutable class,

class A{
private int a;

public A(int a){
    this.a =a ;
}

public int getA(){
    return a;
}
}

How can a class which extends from A , compromise A's immutability ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

貪欢 2024-11-25 15:30:30

像这样:

public class B extends A {
    private int b;

    public B() {
        super(0);
    }

    @Override
    public int getA() {
        return b++;
    }
}

从技术上讲,您不会修改从 A 继承的字段,但在不可变对象中,重复调用同一 getter 当然会产生相同的数字,但事实并非如此这里。

当然,如果您坚持规则#1,则不允许您创建此覆盖。但是,您不能确定其他人会遵守该规则。如果您的方法之一采用 A 作为参数并对其调用 getA(),则其他人可能会像上面那样创建类 B 并将其实例传递给您的方法;然后,您的方法将在不知情的情况下修改该对象。

Like this:

public class B extends A {
    private int b;

    public B() {
        super(0);
    }

    @Override
    public int getA() {
        return b++;
    }
}

Technically, you're not modifying the fields inherited from A, but in an immutable object, repeated invocations of the same getter are of course expected to produce the same number, which is not the case here.

Of course, if you stick to rule #1, you're not allowed to create this override. However, you cannot be certain that other people will obey that rule. If one of your methods takes an A as a parameter and calls getA() on it, someone else may create the class B as above and pass an instance of it to your method; then, your method will, without knowing it, modify the object.

不甘平庸 2024-11-25 15:30:30

里氏替换原则指出,子类可以在超类所在的任何地方使用。从客户的角度来看,孩子就是父母。

因此,如果您重写子级中的方法并使其可变,那么您就违反了与希望该方法不可变的父级客户端之间的合同。

The Liskov substitution principle says that sub-classes can be used anywhere that a super class is. From the point of view of clients, the child IS-A parent.

So if you override a method in a child and make it mutable you're violating the contract with any client of the parent that expects it to be immutable.

故人如初 2024-11-25 15:30:30

如果您将一个字段声明为final,则尝试修改该字段或使其保持未初始化状态不仅仅是使其成为编译时错误。

在多线程代码中,如果您与数据竞争共享类 A 的实例(即,没有任何类型的同步,即通过将其存储在全局可用的位置,例如静态字段),某些线程可能会看到 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; }
}

AB 都是不可变的,也就是说您无法在初始化后修改字段 x 的值(让我们忘记反思)。唯一的区别是 A 中的字段 x 被标记为 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);
  }
}

假设两个线程碰巧发现它们正在监视的字段在主线程设置它们后不为空(请注意,虽然这个假设可能看起来微不足道,但 JVM 不能保证!)。

在这种情况下,我们可以确定监视 a 的线程将打印值 1,因为它的 x 字段是最终的 - 所以,构造函数完成后,可以保证所有看到该对象的线程都会看到 x 的正确值。

但是,我们无法确定另一个线程会做什么。规范只能保证它将打印 01。由于该字段不是 final,并且我们没有使用任何类型的同步(synchronized易失性),因此线程可能会看到该字段未初始化并打印 0!另一种可能性是它实际上看到该字段已初始化,并打印 1。它无法打印任何其他值。

另外,可能发生的情况是,如果您继续读取并打印 bgetX() 值,它可能会在之后开始打印 1打印 0 一会儿!在这种情况下,很清楚为什么不可变对象必须有其字段final:从第二个线程的角度来看,b已经改变,即使它应该改变通过不提供设置器来实现不可变!

如果您想保证第二个线程将看到 x 的正确值而不将字段设为 final,您可以声明保存 B 实例的字段 易失性:

class Main {
  // ...
  volatile static B b;
  // ...
}

另一种可能是在设置和读取字段时进行同步,可以通过修改类 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。然而,这些类(线程安全、不可变、包含非最终字段)必须非常小心地设计,并且应该留给专家。

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() 方法首先读取(非最终的) ) 字段哈希。如果它未初始化(即,如果它是0),它将重新计算它的值,并设置它。对于计算了哈希码并写入该字段的线程,它将永远保留该值。

但是,其他线程可能仍会看到该字段为 0,即使线程已将其设置为其他值之后也是如此。在这种情况下,这些其他线程将重新计算哈希值,并获得完全相同的值,然后对其进行设置。

在这里,证明该类的不变性和线程安全性的理由是,每个线程都将获得完全相同的 hashCode() 值,即使它被缓存在非最终字段中,因为它将被重新计算并获得完全相同的值将获得。

所有这些推理都非常微妙,这就是为什么建议在不可变的线程安全类上将所有字段标记为final

If you declare a field final, there's more to it than make it a compile-time error to try to modify the field or leave it uninitialized.

In multithreaded code, if you share instances of your class A with data races (that is, without any kind of synchronization, i.e. by storing it in a globally available location such as a static field), it is possible that some threads will see the value of getA() change!

Final fields are guaranteed (by the JVM specs) to have its values visible to all threads after the constructor finishes, even without synchronization.

Consider these two classes:

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; }
}

Both A and B are immutable, in the sense that you cannot modify the value of the field x after initialization (let's forget about reflection). The only difference is that the field x is marked final in A. You will soon realize the huge implications of this tiny difference.

Now consider the following code:

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);
  }
}

Suppose both threads happen to see that the fields they are watching are not null after the main thread has set them (note that, although this supposition might look trivial, it is not guaranteed by the JVM!).

In this case, we can be sure that the thread that watches a will print the value 1, because its x field is final -- so, after the constructor has finished, it is guaranteed that all threads that see the object will see the correct values for x.

However, we cannot be sure about what the other thread will do. The specs can only guarantee that it will print either 0 or 1. Since the field is not final, and we did not use any kind of synchronization (synchronized or volatile), the thread might see the field uninitialized and print 0! The other possibility is that it actually sees the field initialized, and prints 1. It cannot print any other value.

Also, what might happen is that, if you keep reading and printing the value of getX() of b, it could start printing 1 after a while of printing 0! In this case, it is clear why immutable objects must have its fields final: from the point of view of the second thread, b has changed, even if it is supposed to be immutable by not providing setters!

If you want to guarantee that the second thread will see the correct value for x without making the field final, you could declare the field that holds the instance of B volatile:

class Main {
  // ...
  volatile static B b;
  // ...
}

The other possibility is to synchronize when setting and when reading the field, either by modifying the 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); }
}

or by modifying the code of Main, adding synchronization to when the field b is read and when it is written -- note that both operations must synchronize on the same object!

As you can see, the most elegant, reliable and performant solution is to make the field x final.


As a final note, it is not absolutely necessary for immutable, thread-safe classes to have all their fields final. However, these classes (thread-safe, immutable, containing non-final fields) must be designed with extreme care, and should be left for experts.

An example of this is the class java.lang.String. It has a private int hash; field, which is not final, and is used as a cache for the 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;
}

As you can see, the hashCode() method first reads the (non-final) field hash. If it is uninitialized (ie, if it is 0), it will recalculate its value, and set it. For the thread that has calculated the hash code and written to the field, it will keep that value forever.

However, other threads might still see 0 for the field, even after a thread has set it to something else. In this case, these other threads will recalculate the hash, and obtain exactly the same value, then set it.

Here, what justifies the immutability and thread-safety of the class is that every thread will obtain exactly the same value for hashCode(), even if it is cached in a non-final field, because it will get recalculated and the exact same value will be obtained.

All this reasoning is very subtle, and this is why it is recommended that all fields are marked final on immutable, thread-safe classes.

沉鱼一梦 2024-11-25 15:30:30

如果扩展类,则派生类可能不是不可变的。

如果您的类是不可变的,那么所有字段在创建后都不会被修改。 Final 关键字将强制执行这一点,并使未来的维护者显而易见。

If the class is extended then the derived class may not be immutable.

If your class is immutable, then all fields will not be modified after creation. The final keyword will enforce this and make it obvious to future maintainers.

心病无药医 2024-11-25 15:30:30

添加此答案以指向 确切的答案JVM 规范的部分提到了为什么成员变量需要是最终的才能在不可变类中实现线程安全。这是规范中使用的示例,我认为非常清楚:

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
        } 
    } 
}

同样,来自规范:

类 FinalFieldExample 有一个最终 int 字段 x 和一个非最终 int 字段 y。一个线程可能执行方法编写器,另一个线程可能执行方法读取器。

因为 writer 方法在对象的构造函数完成后写入 f,所以 reader 方法将保证看到 fx 的正确初始化值:它将读取值 3。但是,fy 不是最终的;因此,不能保证 reader 方法看到它的值 4。

Adding this answer to point to the exact section of the JVM spec that mentions why member variables need to be final in order to be thread-safe in an immutable class. Here's the example used in the spec, which I think is very clear:

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
        } 
    } 
}

Again, from the spec:

The class FinalFieldExample has a final int field x and a non-final int field y. One thread might execute the method writer and another might execute the method reader.

Because the writer method writes f after the object's constructor finishes, the reader method will be guaranteed to see the properly initialized value for f.x: it will read the value 3. However, f.y is not final; the reader method is therefore not guaranteed to see the value 4 for it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文