Неизменяемость в Java

#java

Вопрос:

Если мой класс будет неизменяемым, он должен быть final и без какого-либо метода, который изменяет его состояние, и все свойства должны быть частными. Но почему у него должны быть свойства, объявленные как final (например, private final int a )?

Редактировать

Будет ли класс по-прежнему неизменяемым, если у него есть ссылка на объекты, которые не являются неизменяемыми?

Ответ №1:

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

Комментарии:

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

2. @maks, если значения не изменяются вашим классом, то проверка компилятором безвредна. Если вы-или кто-то, кто изменяет класс после вас-случайно изменяете переменную в классе, который вы считаете неизменяемым, то проверка компилятором бесценна.

Ответ №2:

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

Неизменяемость просто означает, что никакие значения в классе не могут быть изменены. Таким образом, частные поля, никакие не частные сеттеры не должны этого делать. Одна из распространенных проблем заключается в том, что если у вашего класса есть коллекция, вы должны вернуть какую-то неизменяемую коллекцию, иначе значения в коллекции могут быть изменены, даже если никто не может изменить ссылку на коллекцию. Это ответ на вторую часть вашего вопроса. Если объявить a final myCollection в своем классе, вы не сможете изменить ссылку на MyCollection, но вы все равно можете изменить коллекцию за пределами класса, если у вас есть геттер (что нормально) и вы не возвращаете неизменяемую коллекцию. Видишь http://download.oracle.com/javase/6/docs/api/java/util/Collections.html#unmodifiableCollection(java.util.Коллекция)

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

Комментарии:

1. Первый абзац неверен. Завершение неизменяемого класса гарантирует, что изменяемые подклассы не могут быть созданы. Это позволяет получателям ссылок на неизменяемый тип класса полагаться на неизменность фактического объекта. Джошуа Блох подчеркивает это в «Эффективной Java».

2. @andy, но ничто в том, чтобы иметь конечный класс, не делает экземпляры класса неизменяемыми. Разве нет случаев, когда можно было бы захотеть подклассы неизменяемого класса? Я согласен, что это способ гарантировать, что у вас не может быть изменяемого подкласса

3. Большая часть того, что делает неизменяемость полезной, заключается в том, что получатели объекта неизменяемого типа могут полагаться на то, что фактический объект является неизменяемым. (Например, класс, который получает тип, используемый им в качестве ключа на карте.) Подклассы могут быть изменяемыми. Таким образом, заключительные классы действительно имеют какое-то отношение к неизменности-они являются одной из частей гарантии.

4. Второй абзац, второе предложение — Блох перечисляет несколько других методов, гарантирующих неизменное поведение, таких как защитное копирование объектов, переданных конструкторам или возвращенных.

5. @энди, да, я переформулировал свой первый абзац, чтобы включить в него ваши идеи по поводу заключительных занятий… Основная идея неизменяемости в классах означает, что после установки значения не могут быть изменены. Вы можете написать книгу (ну, по крайней мере, здоровую главу) о лучших методах обеспечения неизменности, что выходит за рамки этого ответа. Я все еще немного изменю свой ответ, чтобы указать, что коллекции-это не единственное, о чем стоит беспокоиться…

Ответ №3:

Из сообщения в блоге Джереми Мэнсона Неизменность на Java:

Теперь, в просторечии, неизменность означает «не меняется». Неизменяемость не означает «не меняется» в Java. Это означает, что «транзитивно достижимо из конечного поля, не изменилось с момента установки последнего поля, и ссылка на объект, содержащий последнее поле, не ускользнула от конструктора».

Мэнсон был соавтором спецификации модели памяти Java, поэтому должен знать, о чем он говорит.

Пример из книги Брайана Гетца «Параллелизм Java на практике«:

 public Holder holder;
public void initialize() { holder = new Holder(42); }

publc class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n) {
      throw new AssertionError(”This statement is false”);
    }
  }
}

// Thread 1:             // Thread 2:
initialize();            if (holder != null) holder.assertSanity();
 

Нить 2 выше можно выкинуть AssertionError . Чтобы избежать таких проблем с видимостью n Holder , должно было быть объявлено поле класса final .

ИЗМЕНИТЬ: Рассмотрим последовательность шагов:

  1. Выделите память для нового объекта типа Holder
  2. Инициализировать поле n нового объекта
  3. Выполните конструктор
  4. Назначьте ссылку на новый объект holder ссылке

Это «нормальная» последовательность событий, и JMM (модель памяти Java) гарантирует, что именно это увидит вызывающий поток initialize() . Но, поскольку синхронизации нет, JMM не дает никаких гарантий относительно того, какую последовательность увидит второй вызывающий поток holder.assertSanity() . В частности, AssertionError будет выброшено, если второй поток увидит последовательность:

  1. holder не равно нулю, так что он может вызывать assertSanity метод
  2. Первое считывание n результатов в 0, значение int поля по умолчанию
  3. Второе чтение n результатов 42

Короче говоря, когда конструктор объекта возвращает в одном потоке, другой поток может не увидеть все инициализации полей этого объекта. Чтобы гарантировать, что второй поток увидит правильно инициализированные поля , вы должны либо сделать их final , либо разблокировать мьютекс (т. е. initialize() метод должен быть synchronized ), либо выполнить volatile запись (т. е. holder должен быть volatile ).

Комментарии:

1. мне непонятно, почему в приведенном выше примере может возникнуть исключение. Вы можете объяснить?

Ответ №4:

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

Однако нет никаких оснований считать их не окончательными. Придание им окончательного вида позволяет компилятору проверить ваш код и предотвратить случайное изменение. Это также документирует тот факт, что вы не хотите, чтобы эти свойства записывались более одного раза (при создании).

Комментарии:

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

2. @макс, это верно. Есть 1 случай, о котором я знаю, где final это требуется . Это означает использование локальной переменной (объявленной в методе) в анонимном внутреннем классе в том же методе (как вы бы сделали с прослушивателями Swing). Он должен быть окончательным, так как слушатель вызывается в далеком месте и времени, и эта переменная просто больше не существует, когда вызов слушателя завершен.