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

Чем обеспечивается иммутабельность?

1.7 Middle🔥 101 комментариев
#ООП#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как обеспечивается иммутабельность в Java

Иммутабельность (неизменяемость) - критическое свойство для многопоточности, безопасности и функционального программирования. Это не просто final, это комплексная практика.

1. final поле и класс

Это основа, но не достаточно:

// ❌ НЕПРАВИЛЬНО - кажется неизменяемым
public class User {
    private final String name;
    private final List<String> tags;  // final, но List изменяемый!
    
    public User(String name, List<String> tags) {
        this.name = name;
        this.tags = tags;  // Опасно!
    }
    
    public List<String> getTags() {
        return tags;  // Можно изменить: tags.add("bad")
    }
}

// Использование
List<String> initialTags = new ArrayList<>(Arrays.asList("java"));
User user = new User("John", initialTags);
initialTags.add("hacker");  // Модифицирован объект внутри!
user.getTags().add("malicious");  // Ещё больше модификаций!

Проблема: final только защищает ссылку, не содержимое объекта.

2. Правильная иммутабельность

Требует 5 правил:

// ✅ ПРАВИЛЬНО
public final class User {  // final класс (правило 1)
    private final String name;  // final поле (правило 2)
    private final List<String> tags;
    
    public User(String name, List<String> tags) {
        this.name = name;
        // Правило 3: Защита конструктора - копируем входные данные
        this.tags = new ArrayList<>(tags);  // Копируем!
    }
    
    public String getName() {
        return name;  // Правило 4: Simple getter
    }
    
    public List<String> getTags() {
        // Правило 5: Возвращаем неизменяемую коллекцию
        return Collections.unmodifiableList(tags);
    }
}

// Использование
List<String> initialTags = new ArrayList<>(Arrays.asList("java"));
User user = new User("John", initialTags);
initialTags.add("hacker");  // НЕ влияет на user
// user.getTags().add(...);  // UnsupportedOperationException

5 правил иммутабельности:

  1. Класс final - нельзя наследовать
  2. Все поля final - нельзя переизмерить
  3. Нет setter методов - нельзя изменить
  4. Защита конструктора - копируем входные данные
  5. Защита при возврате - возвращаем копии/неизменяемые коллекции

3. Копирование данных (Defensive Copy)

Критично при работе с mutable типами:

// Mutable типы: Date, Array, List, Map, StringBuilder

// ❌ НЕБЕЗОПАСНО
public final class Order {
    private final Date createdAt;
    private final int[] items;
    
    public Order(Date createdAt, int[] items) {
        this.createdAt = createdAt;  // Date изменяемая!
        this.items = items;  // Массив изменяемый!
    }
    
    public Date getCreatedAt() {
        return createdAt;  // Можно изменить: createdAt.setTime(...)
    }
}

// ✅ ПРАВИЛЬНО
public final class Order {
    private final LocalDateTime createdAt;  // Неизменяемый класс
    private final int[] items;
    
    public Order(LocalDateTime createdAt, int[] items) {
        this.createdAt = createdAt;  // Уже неизменяемый
        this.items = items.clone();  // Копируем массив
    }
    
    public LocalDateTime getCreatedAt() {
        return createdAt;  // Безопасно
    }
    
    public int[] getItems() {
        return items.clone();  // Возвращаем копию
    }
}

4. Неизменяемые коллекции

Java 9+ предоставляет удобные методы:

// Java 8 и раньше
List<String> list = Collections.unmodifiableList(new ArrayList<>());
Set<String> set = Collections.unmodifiableSet(new HashSet<>());
Map<String, String> map = Collections.unmodifiableMap(new HashMap<>());

// Java 9+
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b", "c");
Map<String, String> map = Map.of("key1", "val1", "key2", "val2");

// Попытка изменить -> UnsupportedOperationException
// list.add("d");  // Exception

Разница:

  • Collections.unmodifiable* - обёртки вокруг изменяемых коллекций
  • List.of(), Set.of() - истинно неизменяемые (более оптимизированные)

5. Практический пример: Правильный Immutable класс

public final class Person {
    private final String name;
    private final int age;
    private final List<String> emails;
    private final Address address;
    
    public Person(String name, int age, List<String> emails, Address address) {
        // Валидация
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
        
        this.name = name;
        this.age = age;
        
        // Защита коллекций
        this.emails = emails == null ? 
            List.of() :  // Java 9+
            List.copyOf(emails);  // Копируем и делаем неизменяемой
        
        // Если Address тоже неизменяемый
        this.address = address;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public List<String> getEmails() { return emails; }
    public Address getAddress() { return address; }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(emails, person.emails) &&
               Objects.equals(address, person.address);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, emails, address);
    }
}

// Правильный Address
public final class Address {
    private final String street;
    private final String city;
    
    public Address(String street, String city) {
        this.street = Objects.requireNonNull(street);
        this.city = Objects.requireNonNull(city);
    }
    
    public String getStreet() { return street; }
    public String getCity() { return city; }
}

6. Record (Java 14+) - Автоматическая иммутабельность

Record автоматически реализует все правила:

// ✅ Все правила соблюдены автоматически!
public record User(
    String name,
    int age,
    List<String> tags
) {
    // Compact constructor - валидация
    public User {
        Objects.requireNonNull(name);
        if (age < 0) throw new IllegalArgumentException();
        // Автоматическое копирование
        tags = List.copyOf(tags);
    }
}

// equals(), hashCode(), toString() - генерируются автоматически
// Все поля final - автоматически
User user = new User("John", 25, List.of("java", "spring"));
// user.tags().add(...);  // Error - List.of() неизменяемый

Record упрощает:

  • final поля
  • Конструктор с валидацией (compact constructor)
  • equals() и hashCode()
  • toString()

7. Неизменяемость и многопоточность

public final class ThreadSafeData {
    private final String value;
    
    public ThreadSafeData(String value) {
        this.value = value;
    }
    
    public String getValue() {
        return value;  // Безопасно для многопоточности!
    }
}

// Использование в многопоточной среде
ThreadSafeData data = new ThreadSafeData("immutable");

// Много потоков могут читать одновременно - нет race conditions
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        String value = data.getValue();  // Всегда безопасно
        System.out.println(value);
    });
}

Почему это безопасно:

  • Нет изменения объекта
  • final поля видны всем потокам (happens-before)
  • Не нужна синхронизация

8. Immutable vs Defensive Copy

// Immutable - объект НИКОГДА не меняется
public final class ImmutableUser {
    private final String name;  // Никогда не меняется
    
    public ImmutableUser(String name) {
        this.name = name;
    }
}

// Defensive Copy - объект меняется, но копируем
public class MutableUser {
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;  // Возвращаем копию
    }
}

9. Список неизменяемых классов в Java

Примитивы: int, long, double, boolean
String - неизменяемый
Integer, Long, Double - неизменяемы
LocalDateTime, LocalDate - неизменяемы
List.of(), Set.of(), Map.of() - неизменяемы
Collections.unmodifiableList() - обёртки
Records - неизменяемы

Опасные (изменяемые):
Date, Calendar - меняются
ArrayList, HashMap, HashSet - меняются
StringBuilder, StringBuffer - меняются
Mutable POJOs - меняются

10. Профилактика: Как проверить иммутабельность

public class ImmutabilityTest {
    @Test
    void testImmutability() throws Exception {
        List<String> initialTags = new ArrayList<>(Arrays.asList("java"));
        User user = new User("John", initialTags);
        
        // Попытаемся модифицировать входные данные
        initialTags.add("hacker");
        
        // Объект не должен измениться
        assertThat(user.getTags()).containsExactly("java");
    }
    
    @Test
    void testCannotModifyReturnedCollection() {
        User user = new User("John", List.of("java"));
        
        // Попытаемся модифицировать возвращённый список
        assertThatThrownBy(() -> user.getTags().add("hacker"))
            .isInstanceOf(UnsupportedOperationException.class);
    }
}

Итоговые рекомендации

  • Используй final класс и поля - основа
  • Копируй входные данные - защита конструктора
  • Возвращай копии или unmodifiable - защита при вычитывании
  • Используй Record (Java 14+) - автоматическое решение
  • Выбирай неизменяемые типы - LocalDateTime вместо Date
  • Тестируй иммутабельность - убедись что объект действительно неизменяемый
  • Для многопоточности - иммутабельность решает большинство проблем