Java Language
Неизменяемые объекты
Поиск…
замечания
Неизменяемые объекты имеют фиксированное состояние (без сеттеров), поэтому все состояние должно быть известно во время создания объекта.
Хотя это технически не требуется, лучше всего сделать все поля final
. Это сделает безопасный класс потокобезопасным (см. Java Concurrency in Practice, 3.4.1).
В примерах показаны несколько шаблонов, которые могут помочь в достижении этого.
Создание неизменяемой версии типа с использованием защитного копирования.
Некоторые базовые типы и классы в Java существенно изменяемы. Например, все типы массивов являются изменяемыми, а также такими классами, как java.util.Data
. Это может быть неудобно в ситуациях, когда требуется неизменный тип.
Один из способов борьбы с этим - создать неизменяемую оболочку для изменяемого типа. Вот простая оболочка для массива целых чисел
public class ImmutableIntArray {
private final int[] array;
public ImmutableIntArray(int[] array) {
this.array = array.clone();
}
public int[] getValue() {
return this.clone();
}
}
Этот класс работает с помощью защитного копирования, чтобы изолировать изменяемое состояние ( int[]
) от любого кода, который может его изменить:
Конструктор использует
clone()
для создания отдельной копии массива параметров. Если вызывающий объект конструктора впоследствии изменил массив параметров, это не повлияет на состояние объектаImmutableIntArray
.Метод
getValue()
также используетclone()
для создания возвращаемого массива. Если вызывающий абонент должен был изменить массив результатов, это не повлияет на состояние объектаImmutableIntArray
.
Мы могли бы также добавить методы для ImmutableIntArray
для выполнения операций только для чтения на обернутом массиве; например, получить его длину, получить значение по определенному индексу и т. д.
Обратите внимание, что неизменяемый тип-оболочка, реализованный таким образом, не совместим с исходным типом. Вы не можете просто заменить первое для последнего.
Рецепт создания неизменяемого класса
Неизменяемым объектом является объект, состояние которого не может быть изменено. Неизменяемый класс - это класс, экземпляры которого неизменны по дизайну и реализации. Класс Java, который чаще всего представлен в качестве примера неизменяемости, - это java.lang.String .
Ниже приведен стереотипный пример:
public final class Person {
private final String name;
private final String ssn; // (SSN == social security number)
public Person(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
public String getName() {
return name;
}
public String getSSN() {
return ssn;
}
}
Вариант этого заключается в том, чтобы объявить конструктор как private
и вместо этого предоставить public static
заводский метод.
Стандартный рецепт неизменяемого класса выглядит следующим образом:
- Все свойства должны быть заданы в конструкторе (конструкторах) или заводских методах.
- Не должно быть никаких сеттеров.
- Если необходимо включить сеттеры для соображений совместимости интерфейса, они не должны ничего делать или бросать исключение.
- Все свойства должны быть объявлены как
private
иfinal
. - Для всех свойств, которые являются ссылками на изменяемые типы:
- свойство должно быть инициализировано с глубокой копией значения, переданного через конструктор, и
- геттер свойства должен вернуть глубокую копию значения свойства.
- Класс должен быть объявлен
final
чтобы кто-то не создал изменяемый подкласс неизменяемого класса.
Несколько других замечаний:
- Неизменяемость не препятствует обнулению объекта; например,
null
может быть присвоен переменнойString
. - Если свойства неизменяемых классов объявляются
final
, экземпляры по сути являются потокобезопасными. Это делает неизменные классы хорошим строительным блоком для реализации многопоточных приложений.
Типичные дефекты дизайна, которые препятствуют тому, чтобы класс был непреложным
Используя некоторые сеттеры, не устанавливая все необходимые свойства в конструкторе (конструкторах)
public final class Person { // example of a bad immutability
private final String name;
private final String surname;
public Person(String name) {
this.name = name;
}
public String getName() { return name;}
public String getSurname() { return surname;}
public void setSurname(String surname) { this.surname = surname); }
}
Легко показать, что класс Person
не является неизменным:
Person person = new Person("Joe");
person.setSurname("Average"); // NOT OK, change surname field after creation
Чтобы исправить это, просто удалите setSurname()
и реорганизуйте конструктор следующим образом:
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
Не указывать переменные экземпляра как частные, так и окончательные
Взгляните на следующий класс:
public final class Person {
public String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Следующий фрагмент показывает, что указанный класс не является неизменным:
Person person = new Person("Average Joe");
person.name = "Magic Mike"; // not OK, new name for person after creation
Чтобы исправить это, просто пометьте свойство name как private
и final
.
Выявление изменчивого объекта класса в геттере
Взгляните на следующий класс:
import java.util.List;
import java.util.ArrayList;
public final class Names {
private final List<String> names;
public Names(List<String> names) {
this.names = new ArrayList<String>(names);
}
public List<String> getNames() {
return names;
}
public int size() {
return names.size();
}
}
Класс Names
кажется неизменным с первого взгляда, но это не так, как показано в следующем коде:
List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
Names names = new Names(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList = names.getNames();
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"
Это произошло потому, что изменение ссылочного списка, возвращаемого getNames()
может изменить фактический список Names
.
Чтобы исправить это, просто избегайте возврата ссылок на изменяемые объекты ссылочного класса либо путем создания защитных копий, как показано ниже:
public List<String> getNames() {
return new ArrayList<String>(this.names); // copies elements
}
или путем создания геттеров таким образом, чтобы возвращались только другие неизменные объекты и примитивы :
public String getName(int index) {
return names.get(index);
}
public int size() {
return names.size();
}
Конструктор инжекции с объектами (объектами), которые могут быть изменены вне неизменяемого класса
Это вариация предыдущего недостатка. Взгляните на следующий класс:
import java.util.List;
public final class NewNames {
private final List<String> names;
public Names(List<String> names) {
this.names = names;
}
public String getName(int index) {
return names.get(index);
}
public int size() {
return names.size();
}
}
В качестве класса Names
ранее класс NewNames
кажется неизменным с первого взгляда, но это не так, на самом деле следующий фрагмент доказывает обратное:
List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
NewNames names = new NewNames(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"
Чтобы исправить это, как и в предыдущем изъяне, просто сделайте защитные копии объекта, не присваивая его непосредственно неизменному классу, т. Е. Конструктор можно изменить следующим образом:
public Names(List<String> names) {
this.names = new ArrayList<String>(names);
}
Предотвращение переопределения методов класса
Взгляните на следующий класс:
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name;}
}
Класс Person
кажется неизменным с первого взгляда, но предположим, что новый подкласс Person
определен:
public class MutablePerson extends Person {
private String newName;
public MutablePerson(String name) {
super(name);
}
@Override
public String getName() {
return newName;
}
public void setName(String name) {
newName = name;
}
}
теперь изменчивость Person
(im) может быть использована посредством полиморфизма с использованием нового подкласса:
Person person = new MutablePerson("Average Joe");
System.out.println(person.getName()); prints Average Joe
person.setName("Magic Mike"); // NOT OK, person has now a new name!
System.out.println(person.getName()); // prints Magic Mike
Чтобы исправить это, либо пометьте класс как final
чтобы он не мог быть расширен или объявить все его конструкторы (-и) как private
.