Ява: вопрос о неизменности и окончательный - PullRequest
3 голосов
/ 05 июля 2011

Я читаю книгу Эффективная Java

В статье «Сведите к минимуму изменчивость» Джошуа Блох говорит о том, чтобы сделать класс неизменным.

  1. Не предоставляйте никаких методов, которые изменяют состояние объекта - это нормально.

  2. Убедитесь, что класс не может быть расширен. - Нам действительно нужно это сделать?

  3. Сделать все поля окончательными - нам действительно нужно это сделать?

Например, давайте предположим, что у меня есть неизменный класс,

class A{
private int a;

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

public int getA(){
    return a;
}
}

Как класс, выходящий из А, может поставить под угрозу неизменность А?

Ответы [ 5 ]

6 голосов
/ 05 июля 2011

Примерно так:

public class B extends A {
    private int b;

    public B() {
        super(0);
    }

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

Технически, вы не модифицируете поля, унаследованные от A, но в неизменяемом объекте повторные вызовы одного и того же геттера, конечно, ожидают того же самогочисло, которое здесь не так.

Конечно, если вы придерживаетесь правила # 1, вам не разрешено создавать это переопределение.Однако вы не можете быть уверены, что другие люди будут подчиняться этому правилу.Если один из ваших методов принимает A в качестве параметра и вызывает getA() для него, кто-то другой может создать класс B, как указано выше, и передать его экземпляр вашему методу;тогда ваш метод, не зная этого, изменит объект.

4 голосов
/ 05 июля 2011

Принцип подстановки Лискова говорит, что подклассы могут использоваться везде, где есть суперкласс.С точки зрения клиентов, ребенок IS-A родитель.

Таким образом, если вы переопределяете метод в дочернем элементе и делаете его изменяемым, вы нарушаете договор с любым клиентом родительского объекта, который ожидает, что он будет неизменным.

3 голосов
/ 05 июля 2011

Если вы объявляете поле final, это нечто большее, чем сделать его ошибкой во время компиляции, чтобы попытаться изменить поле или оставить его неинициализированным.

В многопоточном коде, если вы совместно используете экземплярываш класс A с гонками данных (то есть без какой-либо синхронизации, т. е. путем сохранения его в глобально доступном месте, например в статическом поле), возможно, что некоторые потоки увидят значение getA() change!

Final гарантируется (в JVM specs ), что его значения будут видны всем потокам после завершения конструктора, даже без синхронизации.

Рассмотрим эти два класса:

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 помечено final в A. Вы скоро поймете огромное значение этой крошечной разницы.

Теперь рассмотрим следующий код:

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 является окончательным - так что, после завершения конструктора, гарантируется, что все потоки, которые видятобъект увидит правильные значения для x.

Однако мы не можем быть уверены в том, что будет делать другой поток.Спецификации могут гарантировать только то, что он напечатает либо 0, либо 1.Так как поле не final, и мы не использовали никакой синхронизации (synchronized или volatile), поток может увидеть поле неинициализированным и вывести 0!Другая возможность состоит в том, что он фактически видит инициализированное поле и печатает 1. Он не может печатать никакие другие значения.

Кроме того, может произойти следующее: если вы продолжите читать и печатать значение getX() изb, он может начать печатать 1 через некоторое время после печати 0!В этом случае ясно, почему неизменяемые объекты должны иметь свои поля final: с точки зрения второго потока, b изменился, даже если предполагается, что он неизменен, не предоставляя сеттеры!

Если вы хотите гарантировать, что второй поток увидит правильное значение для x, не делая поле final, вы можете объявить поле, содержащее экземпляр B volatile:

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 () сначала читает (непоследнее) поле hash.Если он неинициализирован (то есть если он равен 0), он пересчитает свое значение и установит его.Для потока, который вычислил хеш-код и записал в поле, он сохранит это значение навсегда.

Однако другие потоки могут все еще видеть 0 для поля, даже после того, как поток установил его для чего-то еще.В этом случае эти другие потоки пересчитают хэш и получат точно такое же значение , а затем установят его.

Здесь, что оправдывает неизменность и безопасность потока класса, так это то, что каждый поток получит точно такое же значение для hashCode (), даже если он будет кэширован в неконечном поле, потому что он будет пересчитан ибудет получено точно такое же значение.

Все эти рассуждения очень тонкие, и поэтому рекомендуется, чтобы все поля были помечены как окончательные в неизменяемых, поточно-ориентированных классах .

0 голосов
/ 20 июня 2013

Добавление этого ответа для указания на точный раздел спецификации 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.Один поток может выполнить средство записи метода, а другой может выполнить средство чтения метода.

Поскольку метод модуля записи пишет f после завершения работы конструктора объекта, метод средства чтения будет гарантированно видеть правильно инициализированное значение для fx: он будетпрочитайте значение 3. Однако, fy не является окончательным;поэтому для метода чтения не гарантируется, что для него будет установлено значение 4.

0 голосов
/ 05 июля 2011

Если класс расширен, то производный класс может быть неизменным.

Если ваш класс неизменен, то все поля не будут изменены после создания. Последнее ключевое слово обеспечит это и сделает его очевидным для будущих сопровождающих.

...