В Java объекты и примитивные типы делятся на mutable (изменяемые) и immutable (неизменяемые), в зависимости от возможности изменения их состояния после создания.
1. Immutable (неизменяемые) типы:
- Примитивные типы: Все примитивные типы (например,
int
,double
,char
,boolean
) по своей природе неизменяемы, так как они хранят значение непосредственно, и их нельзя модифицировать после присвоения нового значения — это будет просто создание нового значения. - Обертки примитивов: Классы-обертки для примитивов (
Integer
,Double
,Boolean
и т. д.) тоже являются неизменяемыми. Как только объект обертки создан, его значение нельзя изменить. - Классы из библиотеки Java:
- String: Строки в Java неизменяемы. Любое изменение строки создаёт новый объект, а не изменяет существующий.
- Классы из
java.time
: Например,LocalDate
,LocalTime
,LocalDateTime
,Instant
. Они также являются неизменяемыми и предоставляют методы для создания новых объектов на основе изменений. - BigInteger и BigDecimal: Эти классы предоставляют объекты для работы с большими числами и также неизменяемы.
2. Mutable (изменяемые) типы:
- Массивы: Массивы в Java изменяемы. Элементы массива можно изменять, но сам массив имеет фиксированную длину.
- Классы-коллекции: Коллекции из Java API, такие как
ArrayList
,HashSet
,HashMap
и другие, являются изменяемыми. Элементы в коллекциях можно добавлять, удалять и изменять после их создания. - Классы для работы с I/O: Например,
StringBuilder
иStringBuffer
. Эти классы позволяют изменять строки, предоставляя методы для добавления, удаления или замены частей строки. - Пользовательские классы: Если ваш класс содержит изменяемое состояние (например, переменные экземпляра, которые можно изменять после создания объекта), такие классы также считаются изменяемыми. Если вы хотите создать неизменяемый класс, вы должны следовать принципам неизменяемости (например, сделать поля
final
, не предоставлять “setter”-ы и т.д.).
Как сделать класс неизменяемым:
Чтобы создать immutable класс, необходимо:
- Пометить все поля как
private
иfinal
. - Не предоставлять методы, которые могут изменять состояние объекта (например, setter-ы).
- Если объект содержит ссылки на другие объекты, они также должны быть неизменяемыми или делаться копиями при передаче или возврате (чтобы избежать изменения состояния через внешние ссылки).
Пример неизменяемого класса:
public final class ImmutableClass {
private final int value;
private final String name;
public ImmutableClass(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
}
А в чем практическая разница?
Практическая разница между mutable (изменяемыми) и immutable (неизменяемыми) типами в Java связана с управлением состоянием объектов, безопасностью потоков и производительностью. Рассмотрим, в каких ситуациях эти типы применяются и их влияние на код:
1. Immutable (неизменяемые) объекты:
- Безопасность в многопоточности:
Неизменяемые объекты по своей природе безопасны для многопоточности. Если состояние объекта не может изменяться после его создания, то не нужно заботиться о синхронизации доступа к нему между потоками. Все потоки могут безопасно читать объект одновременно, не создавая конфликтов и не требуя блокировок. Пример: КлассString
в Java — неизменяемый. Это делает его использование в многопоточной среде безопасным без дополнительного контроля. - Упрощение программирования:
Неизменяемые объекты ведут себя предсказуемо, так как их состояние нельзя изменить. Это уменьшает количество возможных ошибок, связанных с непредвиденными изменениями данных. Когда объект неизменяем, вы можете уверенно передавать его между методами, зная, что он останется неизменным. Пример: Когда вы передаёте строку в метод, вам не нужно беспокоиться, что внутри метода она изменится. Любая модификация создаст новый объект. - Кэширование и оптимизация:
Поскольку неизменяемые объекты нельзя изменить, их можно безопасно кэшировать и переиспользовать. Например, строки могут кэшироваться в пуле строк, чтобы избежать создания множества дубликатов одинаковых объектов. Пример: В Java используется пул строк (String Pool), который помогает экономить память при работе с одинаковыми строками. - Проблемы с производительностью при частых изменениях:
Неизменяемость может стать проблемой, если требуется часто изменять состояние объекта. Поскольку каждое изменение создает новый объект, это может быть неэффективно с точки зрения памяти и процессорных ресурсов. Пример: Если вы используете строку и много раз её изменяете в цикле, каждый раз создаётся новая строка, что может привести к большому количеству ненужных объектов в памяти. Для таких случаев лучше использоватьStringBuilder
(mutable версия строк).
2. Mutable (изменяемые) объекты:
- Гибкость и производительность при изменении состояния:
Изменяемые объекты позволяют изменять их состояние напрямую, что может быть значительно более производительным при частых изменениях. Нет необходимости каждый раз создавать новый объект, как это происходит с неизменяемыми. Пример: Коллекции, такие какArrayList
, позволяют добавлять, удалять и изменять элементы напрямую, что делает их эффективными для динамических наборов данных. - Проблемы с многопоточностью:
Если изменяемый объект используется в многопоточной среде, необходимо предусмотреть синхронизацию доступа к его состоянию, чтобы избежать несогласованности данных или гонок потоков. Это делает работу с изменяемыми объектами в многопоточной среде сложнее и может потребовать дополнительных усилий по синхронизации. Пример: Если несколько потоков одновременно изменяют списокArrayList
, это может привести к ошибкам, если не применять механизмы синхронизации, такие какCollections.synchronizedList
. - Сложность отладки и отслеживания состояний:
Изменяемые объекты могут быть сложнее в отладке, так как их состояние может изменяться в разных частях программы, и отследить, кто и когда изменил объект, может быть затруднительно. Пример: Если объект коллекции передается в несколько методов, один из методов может изменить его содержимое, что приведет к неожиданным результатам в других частях программы. - Уменьшение нагрузки на память:
Изменяемые объекты позволяют избегать частого создания новых объектов, что может сократить использование памяти, особенно когда требуется много изменений за короткое время. Пример: Для сложных вычислений с множеством промежуточных шагов, классы вродеStringBuilder
или изменяемые коллекции (например,ArrayList
) могут значительно ускорить выполнение программы.
Пример на практике:
Представим задачу, где требуется часто изменять строку:
Использование String
(immutable):
String result = "";
for (int i = 0; i < 10000; i++) {
result += "a"; // каждое добавление создаёт новый объект строки
}
Это решение будет неэффективным, так как каждое добавление создаёт новую строку, что требует больших затрат на создание объектов и сборку мусора.
Использование StringBuilder
(mutable):
StringBuilder result = new StringBuilder();
for (int i = 0; i < 10000; i++) {
result.append("a"); // изменяется существующий объект
}
Здесь вы работаете с одним объектом, и каждый вызов append()
изменяет его напрямую, что существенно экономит память и время.
Вывод:
- Immutable объекты удобны для многопоточных сред, предсказуемы и безопасны, но могут быть неэффективными при частых изменениях.
- Mutable объекты более гибки и эффективны в случаях, когда нужно часто изменять данные, но требуют осторожности при работе в многопоточности.