Неизменяемые классы в Java

В Java неизменяемый объект — это объект, состояние которого не может быть изменено после создания. Неизменяемые объекты — это постоянные представления своих данных без прямой возможности их изменения. Чтобы изменить состояние, мы должны создать новую копию такого объекта с предполагаемыми изменениями.

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

1. Что такое неизменность?

Неизменяемость — это характеристика объектов Java, которая делает их неизменяемыми для будущих изменений после их инициализации. Их внутреннее состояние не может быть изменено каким-либо образом.

Возьмем в пример класс java.lang.String, который является неизменяемым классом. После создания строки мы не можем изменить ее содержимое. Каждый публичный API в классе String возвращает новую строку с измененным содержимым. Исходная строка всегда остается прежней.

String string = "test";String newString = string.toLowerCase(); //Creates a new String

2. Неизменность в коллекциях

Аналогично, для коллекций Java обеспечивает определенную степень неизменности с тремя вариантами:

  • Неизменяемые коллекции
  • Неизменяемые методы фабрики коллекций(Java 9+)
  • Неизменяемые копии(Java 10+)
Collections.unmodifiableList(recordList); //Unmodifiable listList.of(new Record(1, "test")); //Factory methods in Java 9List.copyOf(recordList); //Java 10

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

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

List<Record> list = List.of(new Record(1, "value"));System.out.println(list); //[Record(id=1, name=value)]//list.add(new Record()); //UnsupportedOperationExceptionlist.get(0).setName("modified-value");System.out.println(list); //[Record(id=1, name=modified-value)]
@Data@NoArgsConstructor@AllArgsConstructorclass Record {long id;String name;}

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

3. Как создать неизменяемый класс?

В документации Java есть некоторые рекомендации по написанию неизменяемых классов по этой ссылке. Мы разберемся, что на самом деле означают эти рекомендации.

  • Не предоставляйте методы-сеттеры. Методы-сеттеры предназначены для изменения состояния объекта, чего мы хотим здесь избежать.
  • Сделать все поля final и private. Поля, объявленные private, не будут доступны за пределами класса, а сделав их final, мы гарантируем, что мы не сможем изменить их даже случайно.
  • Не позволяйте подклассам переопределять методы. Самый простой способ — объявить класс как final. Final-классы в Java не могут быть расширены.
  • Особое внимание «неизменяемым классам с изменяемыми полями». Всегда помните, что поля-члены будут либо изменяемыми, либо неизменяемыми. Значения неизменяемых членов(примитивы, классы-оболочки, строки и т. д.) можно безопасно возвращать из методов получения. Для изменяемых членов(POJO, коллекции и т. д.) мы должны скопировать содержимое в новый объект перед возвратом из метода получения.

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

final class Record {private final long id;private final String name;private final List<String> tokens;public Record(long id, String name, List<String> tokens) {this.id = id;this.name = name;this.tokens = tokens;}public long getId() {return id;}public String getName() {return name;}public List<String> getTokens() {return new ArrayList<>(tokens);}@Overridepublic String toString() {return "Record{" +"id=" + id +", name='" + name + '\'' +", tokens=" + tokens +'}';}}

Теперь пришло время протестировать наш класс. Мы попытались добавить новый элемент в список токенов, но исходная запись и ее список остались неизменными.

ArrayList<String> tokens = new ArrayList<>();tokens.add("active");Record record = new Record(1, "value", tokens);System.out.println(record); //Record{id=1, name='value', tokens=[active]}record.getTokens().add("new token");System.out.println(record); //Record{id=1, name='value', tokens=[active]}

4. Неизменяемость с помощью записей Java

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

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

record Record(long id, String name, List<String> tokens){public List<String> tokens() {return new ArrayList<>(tokens);}}

Теперь давайте еще раз проверим неизменность.

ArrayList<String> tokens = new ArrayList<>();tokens.add("active");Record record = new Record(1, "value", tokens);System.out.println(record); //Record{id=1, name='value', tokens=[active]}record.tokens().add("new token");System.out.println(record); ////Record{id=1, name='value', tokens=[active]}

5. Неизменяемые классы в JDK

Помимо написанных вами классов, в самом JDK есть множество неизменяемых классов. Приведен такой список неизменяемых классов в Java.

  • java.яз.Строка
  • Классы-оболочки, такие как Integer, Long, Double и т. д.
  • java.math.BigInteger и java.math.BigDecimal
  • Неизменяемые коллекции, такие как Collections.singletonMap()
  • java.lang.StackTraceElement
  • Перечисления Java
  • java.util.Locale
  • java.util.UUID
  • API даты и времени Java 8 – LocalDate, LocalTime и т. д.
  • типы записей

6. Преимущества

Неизменяемые объекты имеют массу преимуществ перед изменяемыми объектами. Давайте обсудим их.

  • Предсказуемость: гарантирует, что объекты не изменятся из-за ошибок кодирования или сторонних библиотек. Пока мы ссылаемся на структуру данных, мы знаем, что она та же, что и на момент ее создания.
  • Validity: не нужно проверять снова и снова. Как только мы создаем неизменяемый объект и проверяем его validity один раз, мы знаем, что он будет действителен бесконечно.
  • Потокобезопасность: достигается в программе, поскольку ни один поток не может изменять неизменяемые объекты. Это помогает писать код простым способом, не повреждая случайно общие объекты данных.
  • Кэшируемость: может применяться к неизменяемым объектам, не беспокоясь об изменении состояния в будущем. Методы оптимизации, такие как мемоизация, возможны только с неизменяемыми структурами данных.

7. Заключение

В этом уроке мы научились создавать неизменяемый класс Java с изменяемыми объектами и неизменяемыми полями.

В Java неизменяемыми классами являются:

  • просты в изготовлении, тестировании и использовании
  • автоматически потокобезопасны и не имеют проблем с синхронизацией
  • не нужен конструктор копирования
  • не нужна реализация clone()
  • разрешить hashCode() использовать ленивую инициализацию и кэшировать возвращаемое значение
  • не нужно копировать в целях защиты при использовании в качестве поля
  • сделать хорошими ключи карты и элементы набора(эти объекты не должны менять состояние, пока находятся в коллекции)
  • их инвариант класса устанавливается один раз при построении, и его никогда не нужно проверять снова
  • всегда иметь «атомарность отказа»(термин, используемый Джошуа Блохом): если неизменяемый объект выдает исключение, он никогда не остается в нежелательном или неопределенном состоянии

Мы также увидели преимущества, которые неизменяемые классы привносят в приложение. В качестве наилучшей практики проектирования всегда стремитесь сделать классы Java вашего приложения неизменяемыми. Таким образом, вы всегда можете меньше беспокоиться о дефектах, связанных с параллелизмом в вашей программе.

Приятного обучения!!

Прокрутить вверх