简体   繁体   English

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

[英]java : Question regarding immutable and final

I am reading the book Effective Java.我正在阅读《有效Java》这本书。

In an item Minimize Mutability, Joshua Bloch talks about making a class immutable.在一个项目最小化可变性中,Joshua Bloch 谈到了使 class 不可变。

  1. Don't provide any methods that modify the object's state -- this is fine.不要提供任何修改对象的 state 的方法——这很好。

  2. Ensure that the class can't be extended.确保 class 不能扩展。 - 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,

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?从 A 延伸的 class 如何损害 A 的不变性?

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.从技术上讲,您没有修改从A继承的字段,但是在不可变的 object 中,重复调用相同的 getter 肯定会产生相同的数字,但这里不是这种情况。

Of course, if you stick to rule #1, you're not allowed to create this override.当然,如果您坚持规则 #1,则不允许创建此覆盖。 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;如果您的某个方法将A作为参数并对其调用getA() ,则其他人可能会像上面一样创建 class B并将其实例传递给您的方法; then, your method will, without knowing it, modify the object.然后,您的方法将在不知不觉中修改 object。

The Liskov substitution principle says that sub-classes can be used anywhere that a super class is. Liskov 替换原则说子类可以在超级 class 所在的任何地方使用。 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.因此,如果您覆盖子项中的方法并使其可变,则您违反了与期望它是不可变的父项的任何客户的合同。

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.如果您声明一个字段final ,除了让它成为编译时错误来尝试修改该字段或让它保持未初始化之外,还有更多的事情要做。

In multithreaded code, if you share instances of your class A with data races (that is, without any kind of synchronization, ie 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!在多线程代码中,如果您与数据竞争共享 class A的实例(即,没有任何同步,即通过将其存储在全局可用位置,例如 static 字段),则某些线程可能会看到getA()的值改变了!

Final fields are guaranteed (by the JVM specs ) to have its values visible to all threads after the constructor finishes, even without synchronization.在构造函数完成后, Final字段(由JVM 规范)保证其值对所有线程可见,即使没有同步也是如此。

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). AB都是不可变的,因为您不能在初始化后修改字段x的值(让我们忘记反射)。 The only difference is that the field x is marked final in A. You will soon realize the huge implications of this tiny difference.唯一的区别是字段x在 A 中被标记为final 。您很快就会意识到这个微小差异的巨大影响。

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.).假设两个线程在主线程设置它们后碰巧看到它们正在监视的字段不是 null(请注意,虽然这个假设可能看起来微不足道,但 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 .在这种情况下,我们可以确定监视a的线程将打印值1 ,因为它的x字段是 final ——因此,在构造函数完成后,可以保证看到 object 的所有线程都会看到正确的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 .规范只能保证它将打印01 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.由于该字段不是final ,并且我们没有使用任何类型的同步( synchronizedvolatile ),线程可能会看到未初始化的字段并打印 0,另一种可能性是它实际上看到了已初始化的字段。 and prints 1. It cannot print any other value.并打印 1。它不能打印任何其他值。

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

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:如果您想保证第二个线程将看到x的正确值而不使字段final ,您可以声明保存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:另一种可能性是在设置和读取字段时进行同步,方法是修改 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!或者通过修改 Main 的代码,在字段b被读取和写入时添加同步——注意两个操作必须在同一个对象上同步!

As you can see, the most elegant, reliable and performant solution is to make the field x final.如您所见,最优雅、最可靠和高性能的解决方案是使字段x最终化。


As a final note, it is not absolutely necessary for immutable, thread-safe classes to have all their fields final .最后一点,不可变的线程安全类的所有字段都不是绝对必要的 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.这方面的一个例子是 class java.lang.String。 It has a private int hash;它有一个private int hash; field, which is not final, and is used as a cache for the hashCode():字段,它不是最终的,用作 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 .如您所见, hashCode() 方法首先读取(非最终)字段hash If it is uninitialized (ie, if it is 0), it will recalculate its value, and set it.如果它是未初始化的(即如果它是0),它会重新计算它的值,并设置它。 For the thread that has calculated the hash code and written to the field, it will keep that value forever.对于计算 hash 代码并写入字段的线程,它将永远保持该值。

However, other threads might still see 0 for the field, even after a thread has set it to something else.但是,即使在某个线程将其设置为其他值之后,其他线程仍可能会看到该字段的 0。 In this case, these other threads will recalculate the hash, and obtain exactly the same value , then set it.在这种情况下,这些其他线程会重新计算hash,得到完全相同的值,然后设置它。

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.在这里,证明 class 的不变性和线程安全性的理由是每个线程都将获得完全相同的 hashCode() 值,即使它被缓存在非最终字段中,因为它将重新计算并且完全相同的值将获得。

All this reasoning is very subtle, and this is why it is recommended that all fields are marked final on immutable, thread-safe classes .所有这些推理都非常微妙,这就是为什么建议在不可变的线程安全类上将所有字段标记为 final 的原因。

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.添加此答案以指向JVM 规范的确切部分,该部分提到为什么成员变量需要是最终的,以便在不可变的 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. class FinalFieldExample 具有最终 int 字段 x 和非最终 int 字段 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 fx: it will read the value 3. However, fy is not final;因为 writer 方法在对象的构造函数完成后写入 f,所以 reader 方法将保证看到正确初始化的 fx 值:它将读取值 3。 the reader method is therefore not guaranteed to see the value 4 for it.因此,不能保证 reader 方法看到它的值 4。

If the class is extended then the derived class may not be immutable.如果 class 被扩展,则派生的 class 可能不是不可变的。

If your class is immutable, then all fields will not be modified after creation.如果您的 class 是不可变的,那么所有字段在创建后都不会被修改。 The final keyword will enforce this and make it obvious to future maintainers. final 关键字将强制执行这一点,并使其对未来的维护者显而易见。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM