Иммутабельность в 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 .
- Чувствительные данные, такие как имена пользователей и пароли, нельзя изменить по ошибке во время выполнения, даже при передаче ссылок на них между разными методами.
Как реализована неизменность string в java
В Java строка ( String ) является неизменяемым ( immutable ) объектом, то есть после создания строки её нельзя изменить. Когда мы изменяем строку, мы фактически создаем новую строку, которая содержит изменения. Это означает, что операции над строками в Java обычно более безопасны, чем операции над изменяемыми объектами.
Неизменность строк в Java достигается путем хранения строк в виде массива символов ( char[] ), который является неизменяемым объектом. При изменении строки создается новый массив символов с новым значением строки.
Также Java использует кэширование строк, что позволяет экономить память. Если две строки содержат одинаковые символы, то они ссылаются на один и тот же объект в памяти (строка сохраняется в пуле строк). Это возможно благодаря тому, что строка является неизменяемой.
Java: Неизменяемость строк
Кажется, что ответом будет «HEXLET» , но это не так. Эта программа выведет «hexlet» (проверьте на tryjshell). Почему?
Дело в том, что строки в Java неизменяемы. Не существует способа и методов, способных изменить саму строку. Любой метод строки может только вернуть новую строку.
Основная причина, почему так сделано – производительность. Строки, и другие примитивные типы данных нельзя менять практически ни в одном современном языке.
Вторая причина связана с простотой кода. Когда мы не изменяем данные, а создаем новые данные на основе старых, то код проще анализировать и модифицировать. Особенно если с данными происходит много манипуляций, с этим вам еще предстоит столкнуться.
Но как же поступать, если данные нужно поменять? Для этого достаточно заменить значение переменной:
var language = "JAVA"; language = language.toLowerCase(); System.out.println(language); // => java
С другой стороны, именно в такой ситуации можно создать новую переменную с другим именем:
var language = "JAVA"; var processedLanguage = language.toLowerCase(); System.out.println(processedLanguage); // => java
Такой подход нередко предпочтительнее по соображениям читаемости. Переменные, которые постоянно меняются, сложнее анализировать. В итоге все зависит от задачи. С опытом придет понимание, какой подход лучше.
Задание
Данные, вводимые пользователями в формах, часто содержат лишние пробельные символы в конце или начале строки. Кроме того, пользователи могут вводить одно и то же в разном регистре, что потом мешает работе с данными. Поэтому перед тем как добавлять их, данные обрабатывают (говорят нормализуют). В базовую обработку входят два действия:
- Удаление концевых пробельных символов с помощью метода .trim() , например, было: » hexlet\n » , стало: «hexlet»
- Приведение к нижнему регистру с помощью метода toLowerCase() . Было: «SUPPORT@hexlet.io» , стало: «support@hexlet.io» .
Обновите переменную email записав в неё то же самое значение, но обработанное по схеме указанной выше. Распечатайте то, что получилось, на экран.
Упражнение не проходит проверку — что делать?
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя
Это нормально , в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Почему String неизменяемый? и как это помогает программисту?
Что помимо того что нельзя наследоваться от final класса, а именно String.class , и того что при использовании метода concat() и операции + будет создан новый объект типа String а не изменен существующий, используется при определении неизменяемости класса String ? Как неизменяемость (immutable) класса String помогает в реальных ситуациях программисту. Были бы интересны примеры связанные с HashMap и многопоточностью.
Отслеживать
задан 13 июл 2016 в 10:36
1,986 6 6 золотых знаков 29 29 серебряных знаков 48 48 бронзовых знаков
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Вот тут человек уже ответил на ваш вопрос:
- вы можете передавать строку между потоками и не беспокоиться что она будет изменена
- нет проблем с синхронизацией (не нужно синхронизировать операции со String)
- отсутствие утечек памяти
- в Java строки используются для передачи параметров для авторизации, открытия файлов и т.д. — неизменяемость позволяет избежать проблем с доступом
- возможность кэшировать hash code
А если нужно изменять, есть StringBuffer.