← Назад к вопросам
Как реализовать иммутабельность класса с полем с типом List?
2.0 Middle🔥 181 комментариев
#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализовать иммутабельность класса с полем с типом List?
Это классическая проблема: если класс содержит mutable поле (как List), как сделать класс иммутабельным? Ответ: не возвращай саму List, возвращай unmodifiable копию.
Неправильный способ (vulnerable)
public class ImmutableUser {
private final List<String> tags;
public ImmutableUser(List<String> tags) {
this.tags = tags; // ОПАСНО! Ссылка на mutable список
}
public List<String> getTags() {
return tags; // ОПАСНО! Возвращаю оригинальный список
}
}
// Использование:
List<String> myTags = new ArrayList<>();
myTags.add("java");
ImmutableUser user = new ImmutableUser(myTags);
// Атака 1: изменяю через конструктор
myTags.add("hacker"); // ИЗМЕНИЛ через ссылку!
System.out.println(user.getTags()); // [java, hacker] - НЕПРАВИЛЬНО
// Атака 2: изменяю через getTags()
user.getTags().add("hacker"); // ИЗМЕНИЛ список!
System.out.println(user.getTags()); // [java, hacker] - НЕПРАВИЛЬНО
Класс выглядит иммутабельным, но на самом деле он уязвим.
Правильный способ 1: Collections.unmodifiableList()
public class ImmutableUser {
private final List<String> tags;
public ImmutableUser(List<String> tags) {
// Копируем список и оборачиваем в unmodifiable
this.tags = Collections.unmodifiableList(new ArrayList<>(tags));
}
public List<String> getTags() {
// Возвращаем unmodifiable обертку
return tags;
}
}
// Использование:
List<String> myTags = new ArrayList<>();
myTags.add("java");
ImmutableUser user = new ImmutableUser(myTags);
// Атака 1: изменяю через конструктор - БЕЗ ЭФФЕКТА
myTags.add("hacker");
System.out.println(user.getTags()); // [java] - ЗАЩИЩЕНО!
// Атака 2: пытаюсь изменить через getTags()
try {
user.getTags().add("hacker"); // UnsupportedOperationException!
} catch (UnsupportedOperationException e) {
System.out.println("Попытка изменить иммутабельный список");
}
Как это работает:
- В конструкторе: копируем входной список → создаёшь новый ArrayList
- Оборачиваем копию в Collections.unmodifiableList()
- При getTags(): возвращаем эту unmodifiable обертку
- Любая попытка изменения выбрасывает UnsupportedOperationException
Правильный способ 2: List.of() (Java 9+)
Это более современный способ:
public class ImmutableUser {
private final List<String> tags;
public ImmutableUser(List<String> tags) {
// List.of создает immutable список
this.tags = List.of(tags.toArray(new String[0]));
}
public List<String> getTags() {
return tags;
}
}
// Использование:
List<String> myTags = new ArrayList<>();
myTags.add("java");
ImmutableUser user = new ImmutableUser(myTags);
// Попытка изменить
try {
user.getTags().add("hacker"); // UnsupportedOperationException!
} catch (UnsupportedOperationException e) {
System.out.println("Защищено");
}
Разница List.of() vs Collections.unmodifiableList():
- List.of() создает по-настоящему иммутабельный список (null-safe)
- Collections.unmodifiableList() создает wrapper вокруг существующего списка
- List.of() немного быстрее
- List.of() не позволяет null элементы
Правильный способ 3: Копия + Stream (для сложных случаев)
public class ImmutableUser {
private final List<String> tags;
public ImmutableUser(List<String> tags) {
// Можем фильтровать при копировании
this.tags = tags.stream()
.filter(tag -> tag != null && !tag.isEmpty())
.collect(Collectors.toUnmodifiableList());
}
public List<String> getTags() {
return tags;
}
// Дополнительная безопасность
public List<String> getTagsLowerCase() {
return tags.stream()
.map(String::toLowerCase)
.collect(Collectors.toUnmodifiableList());
}
}
Полный пример: Record (Java 14+)
Record автоматически генерирует иммутабельные getter'ы:
public record ImmutableUser(
String name,
List<String> tags,
int age
) {
// Compact constructor для валидации
public ImmutableUser {
Objects.requireNonNull(name);
Objects.requireNonNull(tags);
// Копируем список
tags = Collections.unmodifiableList(new ArrayList<>(tags));
}
// Getters автоматически создаются: name(), tags(), age()
}
// Использование:
ImmutableUser user = new ImmutableUser(
"John",
new ArrayList<>(Arrays.asList("java", "spring")),
30
);
// Безопасно
user.tags().add("hacker"); // UnsupportedOperationException
Практический пример: Immutable класс для Domain
public final class Order {
private final Long id;
private final String orderId;
private final List<OrderItem> items;
private final BigDecimal totalAmount;
private final Instant createdAt;
public Order(
Long id,
String orderId,
List<OrderItem> items,
BigDecimal totalAmount,
Instant createdAt
) {
this.id = Objects.requireNonNull(id);
this.orderId = Objects.requireNonNull(orderId);
this.items = Collections.unmodifiableList(
new ArrayList<>(Objects.requireNonNull(items))
);
this.totalAmount = Objects.requireNonNull(totalAmount);
this.createdAt = Objects.requireNonNull(createdAt);
}
// Только getter'ы, NO setter'ы
public Long getId() {
return id;
}
public String getOrderId() {
return orderId;
}
public List<OrderItem> getItems() {
return items; // Уже unmodifiable
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public Instant getCreatedAt() {
return createdAt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order order)) return false;
return Objects.equals(id, order.id) &&
Objects.equals(orderId, order.orderId) &&
Objects.equals(items, order.items) &&
Objects.equals(totalAmount, order.totalAmount) &&
Objects.equals(createdAt, order.createdAt);
}
@Override
public int hashCode() {
return Objects.hash(id, orderId, items, totalAmount, createdAt);
}
@Override
public String toString() {
return "Order{" +
"id=" + id +
", orderId='" + orderId + '\'' +
", items=" + items +
", totalAmount=" + totalAmount +
", createdAt=" + createdAt +
'}';
}
}
public final class OrderItem {
private final String productName;
private final int quantity;
private final BigDecimal price;
public OrderItem(String productName, int quantity, BigDecimal price) {
this.productName = Objects.requireNonNull(productName);
this.quantity = quantity;
this.price = Objects.requireNonNull(price);
}
// Getters
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public BigDecimal getPrice() { return price; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderItem item)) return false;
return quantity == item.quantity &&
Objects.equals(productName, item.productName) &&
Objects.equals(price, item.price);
}
@Override
public int hashCode() {
return Objects.hash(productName, quantity, price);
}
}
Использование Lombok для упрощения
import lombok.Value;
import lombok.NonNull;
import java.util.List;
import java.util.Collections;
@Value // Автоматически final + getter'ы + equals/hashCode
public class ImmutableUser {
@NonNull
String name;
@NonNull
List<String> tags;
// Lombok генерирует безопасный конструктор
// но нужно вручную обернуть List
public ImmutableUser(String name, List<String> tags) {
this.name = name;
this.tags = Collections.unmodifiableList(new ArrayList<>(tags));
}
}
Правила для иммутабельности
1. Класс FINAL (no inheritance)
2. Все поля FINAL
3. Нет setter'ов
4. Mutable поля (List, Set, Map):
- Копируй в конструкторе
- Оборачивай в unmodifiable
- Возвращай unmodifiable версию
5. Mutable поля других типов (Date):
- Копируй (new Date(date.getTime()))
6. Валидируй в конструкторе
7. Реализуй equals() и hashCode()
8. Рассмотри toString()
Тестирование иммутабельности
@Test
public void testImmutability() {
List<String> tags = new ArrayList<>();
tags.add("java");
ImmutableUser user = new ImmutableUser("John", tags);
// Попытка 1: изменить источник
tags.add("hacker");
assertThat(user.getTags()).containsExactly("java"); // ✓
// Попытка 2: изменить через getter
assertThatThrownBy(() -> user.getTags().add("hacker"))
.isInstanceOf(UnsupportedOperationException.class); // ✓
}
Сравнение подходов
| Подход | Плюсы | Минусы |
|---|---|---|
| Collections.unmodifiableList() | Проверенный, работает везде | Verbose код |
| List.of() (Java 9+) | Современный, null-safe | Не поддерживает старые версии |
| Record (Java 14+) | Минимум кода, автоматический | Нужна ручная работа с List |
| Lombok @Value | Краткий код | Потребление memory |
Заключение
Для иммутабельности с List:
- Копируй входной список при инициализации
- Оборачивай в unmodifiable (или используй List.of())
- Возвращай unmodifiable версию из getter'а
- Класс должен быть final
- Нет setter'ов
- Валидируй в конструкторе
- Реализуй equals() и hashCode()
- Рассмотри использование Record или Lombok для упрощения