← Назад к вопросам

Как реализовать иммутабельность класса с полем с типом 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("Попытка изменить иммутабельный список");
}

Как это работает:

  1. В конструкторе: копируем входной список → создаёшь новый ArrayList
  2. Оборачиваем копию в Collections.unmodifiableList()
  3. При getTags(): возвращаем эту unmodifiable обертку
  4. Любая попытка изменения выбрасывает 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:

  1. Копируй входной список при инициализации
  2. Оборачивай в unmodifiable (или используй List.of())
  3. Возвращай unmodifiable версию из getter'а
  4. Класс должен быть final
  5. Нет setter'ов
  6. Валидируй в конструкторе
  7. Реализуй equals() и hashCode()
  8. Рассмотри использование Record или Lombok для упрощения
Как реализовать иммутабельность класса с полем с типом List? | PrepBro