简体   繁体   中英

java : Question regarding immutable and final

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?

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.

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.

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, 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!

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 .

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 fx: it will read the value 3. However, fy is not final; the reader method is therefore not guaranteed to see the value 4 for it.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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