Как написать иммутабельный класс?
Immutable (неизменяемый) класс – это класс, состояние экземпляров которого невозможно изменить после создания.
С иммутабельным классом всегда легче работать. Его состояние не поменяется, значит обращаться к нему в многопоточной среде можно без дополнительной синхронизации. Функции, зависящие только от состояния экземпляра будут возвращать один и тот же результат от вызова к вызову – это облегчает например реализацию hashCode(). Также вместо нескольких одинаковых экземпляров можно использовать один закэшированный объект, экономя память (паттерн Приспособленец).
Шаги, которые необходимо предпринять, чтобы класс стал immutable:
1. Запретите расширение класса – либо объявите его final , либо закройте доступ наследникам ко всем способам мутации, перечисленным в следующих пунктах;
2. Сделайте все поля финальными;
3. Не выставляйте наружу методов-мутаторов, которые меняют состояние;
4. Не отдавайте наружу поля ссылочного изменяемого типа (объекты классов, массивы) – если объект под ссылкой не иммутабельный, должна возвращаться его глубокая копия (defensive copy);
5. Создавайте объект правильно (подробнее в следующем посте).
Если вам нужны преимущества иммутабельного объекта, но также нужно иногда изменять его, подойдет подход copy on write: каждый метод-мутатор должен мутировать и возвращать не сам объект, а только что созданную его копию. Оригинал всё так же остается неизменным.
Как сделать иммутабельный класс java
«Иммутабельный класс» означает неизменяемый. Например, String является иммутабельным классом.
Для того чтобы класс считался иммутабельным — все его поля должны быть final . То есть любое изменение внутреннего состояния объекта, на основании иммутабельного класса, влечет за собой всегда создание нового объекта этого же класса.
Начиная с java 14 появился дополнительный синтаксис для создания неизменяемых объектов: java records .
Иммутабельность в Java
Привет, Хабр. В преддверии скорого старта курса «Подготовка к сертификации Oracle Java Programmer (OCAJP)» подготовили для вас традиционный перевод материала.
Приглашаем также всех желающих поучаствовать в открытом демо-уроке «Конструкторы и блоки инициализации». На этом бесплатном вебинаре мы:
— Разберём конструктор на запчасти
— Определим финалистов (финальные переменные)
— Наведём порядок (инициализации)

Иммутабельный (неизменяемый, immutable) класс — это класс, который после инициализации не может изменить свое состояние. То есть если в коде есть ссылка на экземпляр иммутабельного класса, то любые изменения в нем приводят к созданию нового экземпляра.
Чтобы класс был иммутабельным, он должен соответствовать следующим требованиям:
- Должен быть объявлен как final, чтобы от него нельзя было наследоваться. Иначе дочерние классы могут нарушить иммутабельность.
- Все поля класса должны быть приватными в соответствии с принципами инкапсуляции.
- Для корректного создания экземпляра в нем должны быть параметризованные конструкторы, через которые осуществляется первоначальная инициализация полей класса.
- Для исключения возможности изменения состояния после инстанцирования, в классе не должно быть сеттеров.
- Для полей-коллекций необходимо делать глубокие копии, чтобы гарантировать их неизменность.
Иммутабельность в действии
Начнем со следующего класса, который, на первый взгляд, выглядит иммутабельным:
import java.util.Map; public final class MutableClass < private String field; private MapfieldMap; public MutableClass(String field, Map fieldMap) < this.field = field; this.fieldMap = fieldMap; >public String getField() < return field; >public Map getFieldMap() < return fieldMap; >>
Теперь посмотрим на него в действии.
import java.util.HashMap; import java.util.Map; public class App < public static void main(String[] args) < Mapmap = new HashMap<>(); map.put("key", "value"); // Инициализация нашего "иммутабельного" класса MutableClass mutable = new MutableClass("this is not immutable", map); // Можно легко добавлять элементы в map == изменение состояния mutable.getFieldMap().put("unwanted key", "another value"); mutable.getFieldMap().keySet().forEach(e -> System.out.println(e)); > > // Вывод в консоли unwanted key key
Очевидно, что мы хотим запретить добавление элементов в коллекцию, поскольку это изменение состояния объекта, то есть отсутствие иммутабельности.
import java.util.HashMap; import java.util.Map; public class AlmostMutableClass < private String field; private MapfieldMap; public AlmostMutableClass(String field, Map fieldMap) < this.field = field; this.fieldMap = fieldMap; >public String getField() < return field; >public Map getFieldMap() < MapdeepCopy = new HashMap(); for(String key : fieldMap.keySet()) < deepCopy.put(key, fieldMap.get(key)); >return deepCopy; > >
Здесь мы изменили метод getFieldMap , который теперь возвращает глубокую копию коллекции, ссылка на которую есть в AlmostMutableClass . Получается, что если мы получим Map , вызвав метод getFieldMap , и добавим к нему элемент, то на map из нашего класса это никак не повлияет. Изменится только map , которую мы получили.
Однако если у нас остается доступ к исходной map , которая была передана в качестве параметра конструктору, то все не так уж и хорошо. Мы можем изменить ее, тем самым изменив состояние объекта.
import java.util.HashMap; import java.util.Map; public class App < public static void main(String[] args) < Mapmap = new HashMap<>(); map.put("good key", "value"); // Инициализация нашего "иммутабельного" класса AlmostMutableClass almostMutable = new AlmostMutableClass("this is not immutable", map); // Мы не можем изменять состояние объекта // через добавление элементов в полученную map System.out.println("Result after modifying the map after we get it from the object"); almostMutable.getFieldMap().put("bad key", "another value"); almostMutable.getFieldMap().keySet().forEach(e -> System.out.println(e)); System.out.println("Result of the object's map after modifying the initial map"); map.put("bad key", "another value"); almostMutable.getFieldMap().keySet().forEach(e -> System.out.println(e)); > > // Вывод в консоли Result after modifying the map after we get it from the object good key Result of the object's map after modifying the initial map good key bad key
Мы забыли, что в конструкторе нужно сделать то же самое, что и в методе getFieldMap . В итоге конструктор должен выглядеть так:
public AlmostMutableClass(String field, Map fieldMap) < this.field = field; MapdeepCopy = new HashMap(); for(String key : fieldMap.keySet()) < deepCopy.put(key, fieldMap.get(key)); >this.fieldMap = deepCopy; > // Вывод в консоли Result after modifying the map after we get it from the object good key Result of the object's map after modifying the initial map good key
Хотя использование иммутабельных объектов дает преимущества, но их использование не всегда оправдано. Обычно нам нужно как создавать объекты, так и модифицировать их для отражения изменений, происходящих в системе.
То есть нам нужно изменять данные, и нелогично создавать новые объекты при каждом изменении, так как это увеличивает используемую память, а мы хотим разрабатывать эффективные приложения и оптимально использовать ресурсы системы.
Иммутабельность строк в Java
Класс String , представляющий набор символов, вероятно, самый популярный класс в Java. Его назначение — упростить работу со строками, предоставляя различные методы для их обработки.
Например, в классе String есть методы для получения символов, выделения подстрок, поиска, замены и многие другие. Как и другие классы-обертки в Java (Integer, Boolean и т.д.), класс String является иммутабельным.
Иммутабельность строк дает следующие преимущества:
- Строки потокобезопасны.
- Для строк можно использовать специальную область памяти, называемую «пул строк». Благодаря которой две разные переменные типа String с одинаковым значением будут указывать на одну и ту же область памяти.
- Строки отличный кандидат для ключей в коллекциях, поскольку они не могут быть изменены по ошибке.
- Класс String кэширует хэш-код, что улучшает производительность хеш-коллекций, использующих String .
- Чувствительные данные, такие как имена пользователей и пароли, нельзя изменить по ошибке во время выполнения, даже при передаче ссылок на них между разными методами.
Неизменяемые объекты в Java
Неизменяемый объект — это объект, который не изменит своего внутреннего состояния после создания.
Неизменяемые объекты очень полезны в многопоточных приложениях, потому что они могут совместно использоваться потоками без синхронизации .
Неизменяемые объекты всегда безопасны для потоков.
Темы везде
Неважно, пишете вы явное многопоточное приложение или нет, часто вы работаете в многопоточной среде без непосредственного управления экземплярами потоков.
Вот несколько примеров многопоточных приложений, в которых программист не вызывает вручную создание новых потоков:
- Веб-приложения, потому что сервлеты обрабатываются пулом потоков
- Качайте настольные приложения, где потоки обрабатывают события GUI
- Экземпляры таймера создают новые потоки для обработки будущих задач
Создание неизменного объекта
Чтобы создать неизменный объект, вам нужно следовать нескольким простым правилам:
- Не добавляйте метод установки
- Объявите все поля как окончательные и закрытые
- Если поле является изменяемым объектом, создайте его защитные копии для методов получения
- Если изменяемый объект, переданный конструктору, должен быть присвоен полю, создайте его защитную копию
- Не позволяйте подклассам переопределять методы.
Теперь мы обнаруживаем, точка за точкой, причины пяти правил, объясненных ранее.
Не добавляйте метод установки
Если вы строите неизменный объект, его внутреннее состояние никогда не изменится. Задача метода-установщика состоит в том, чтобы изменить внутреннее значение поля, чтобы вы не могли его добавить.
Объявите все поля как окончательные и закрытые
Закрытое поле не видно снаружи класса, поэтому никакие ручные изменения не могут быть применены к нему.
Объявление поля final гарантирует, что если оно ссылается на примитивное значение, значение никогда не изменится, если оно ссылается на объект, ссылка не может быть изменена. Этого недостаточно, чтобы гарантировать, что объект с только частными конечными полями не является изменяемым. Вот пример, показывающий объект с приватным конечным полем и пример того, как изменить его внутреннее состояние:
public class DateContainer private final Date date; public DateContainer() this.date = new Date(); > public Date getDate() return date; > > . DateContainer dateContainer = new DateContainer(); System.out.println(dateContainer.getDate()); dateContainer.getDate().setTime(dateContainer.getDate().getTime() + 1000); System.out.println(dateContainer.getDate()); // Now dateContainer date is 1 second after
Если поле является изменяемым объектом, создайте его защитные копии для методов получения
Ранее мы видели, что определения поля final и private недостаточно, потому что можно изменить его внутреннее состояние. Чтобы решить эту проблему, нам нужно создать защитную копию этого поля и возвращать это поле каждый раз, когда оно запрашивается.
Вот предыдущий класс с этой модификацией:
public class DateContainer private final Date date; public DateContainer() this.date = new Date(); > public Date getDate() return new Date(date.getTime()); > > . DateContainer dateContainer = new DateContainer(); System.out.println(dateContainer.getDate()); dateContainer.getDate().setTime(dateContainer.getDate().getTime() + 1000); System.out.println(dateContainer.getDate()); // Now dateContainer date is not changed because we changed the copy, // not the original date
Если изменяемый объект, переданный конструктору, должен быть присвоен полю, создайте его защитную копию
Та же проблема возникает, если вы держите ссылку, переданную конструктору, потому что ее можно изменить.
Здесь мы показываем модифицированный пример DateContainer, который принимает Date для конструктора, и мы увидим, как можно изменить его внутреннее состояние:
public class DateContainer private final Date date; public DateContainer(Date date) this.date = date; > public Date getDate() return new Date(date.getTime()); > > . Date date = new Date(); DateContainer dateContainer = new DateContainer(date); System.out.println(dateContainer.getDate()); date.setTime(date.getTime() + 1000); System.out.println(dateContainer.getDate()); // Now dateContainer date is 1 second after also if the getter method // create a defensive copy of date. We changed the reference passed to the // constructor, not the copy.
Таким образом, удерживая ссылку на объект, переданный конструктору, можно создавать изменяемые объекты. Для решения этой проблемы необходимо создать защитную копию параметра, если они являются изменяемыми объектами:
public class DateContainer private final Date date; public DateContainer(Date date) this.date = new Date(date.getTime()); > public Date getDate() return new Date(date.getTime()); > > . Date date = new Date(); DateContainer dateContainer = new DateContainer(date); System.out.println(dateContainer.getDate()); date.setTime(date.getTime() + 1000); System.out.println(dateContainer.getDate()); // Now dateContainer date is not changed. We create a copy on the constructor // so a change to the external date will not affect the internal state of // DateContainer instance
Обратите внимание, что если поле является ссылкой на неизменный объект, нет необходимости создавать его защитные копии в конструкторе и в методах получения, достаточно определить поле как окончательное и закрытое. В качестве примера общих неизменяемых объектов можно привести String, все примитивные оболочки (Integer, Long, Double, Byte ….), BigDecimal, BigInteger.
Не позволяйте подклассам переопределять методы
Если подкласс переопределяет метод, он может вернуть исходное значение изменяемого поля вместо его защитной копии.
Для решения этой проблемы можно выполнить одно из следующих действий:
- Объявите неизменный класс как final, чтобы он не мог быть расширен
- Объявите все методы неизменяемого класса final, чтобы они не могли быть переопределены
- Создайте приватный конструктор и фабрику для создания экземпляров неизменяемого класса, потому что класс с приватными конструкторами не может быть расширен
Если вы будете следовать этим простым правилам, вы можете свободно делиться своими неизменяемыми объектами между потоками, потому что они потокобезопасны!