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

Достаточно ли сделать класс final, поля private и не использовать сеттеры для обеспечения неизменяемости объекта после его создания

3.0 Senior🔥 121 комментариев
#ООП

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

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

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

Неизменяемость: final класс + private поля + без setters

Это ХОРОШИЙ подход, но недостаточный для полной гарантии неизменяемости. Рассмотрим, почему и что ещё нужно.

Проблема: Изменяемые поля в private

public final class Point {
    private final List<Integer> coordinates;  // List изменяемый!
    
    public Point(List<Integer> coords) {
        this.coordinates = coords;  // Не скопировали
    }
    
    public List<Integer> getCoordinates() {
        return coordinates;  // Возвращаем ссылку на внутренний список
    }
}

// Использование
List<Integer> coords = Arrays.asList(10, 20);
Point p = new Point(coords);

// Менять можно через исходный список
coords.set(0, 999);
// Или через getter
p.getCoordinates().set(1, 999);

System.out.println(p.getCoordinates());  // [999, 999] — объект изменился!

Решение: Оборонительное копирование и неизменяемые обёртки.

Правильный подход: Полная защита

public final class Point {  // 1. Класс final
    private final List<Integer> coordinates;  // 2. Поля final
    
    public Point(List<Integer> coords) {
        // 3. Защита при ВХОДЕ: оборонительное копирование
        this.coordinates = Collections.unmodifiableList(
            new ArrayList<>(coords)  // Копируем и оборачиваем
        );
    }
    
    // 4. Нет setters
    
    // 5. Защита при ВЫХОДЕ: возвращаем неизменяемую копию
    public List<Integer> getCoordinates() {
        return coordinates;  // Уже unmodifiable, но можно и ещё раз обернуть
    }
}

// Теперь безопасно
List<Integer> coords = new ArrayList<>();
coords.add(10);
coords.add(20);

Point p = new Point(coords);

// Пытаемся менять исходный список
coords.set(0, 999);  // OK, но не повлияет на Point

// Пытаемся менять через getter
p.getCoordinates().set(1, 999);  // UnsupportedOperationException!

Типы изменяемых объектов (требуют защиты)

// ❌ ОПАСНЫЕ типы (изменяемые)
List<T>              // ArrayList, LinkedList, etc
Set<T>               // HashSet, TreeSet, etc
Map<K, V>            // HashMap, TreeMap, etc
StringBuilder        // Изменяемый string
Date                 // Старый класс, изменяемый

// ✅ БЕЗОПАСНЫЕ типы (неизменяемые)
String               // Immutable
Integer, Long, etc   // Immutable
LocalDate, ZonedDateTime  // Immutable (java.time)
Collections.unmodifiableList(...)  // Wrapper
Collections.unmodifiableMap(...)
Collections.unmodifiableSet(...)

Практический пример: Полностью защищённый класс

public final class User {  // 1. final
    private final String name;  // 2. final простой тип (безопасен)
    private final int age;      // 2. final примитив (безопасен)
    private final LocalDate birthDate;  // 2. final immutable тип (безопасен)
    private final List<String> roles;   // 2. final, но List изменяемый!
    private final Map<String, String> attributes;  // 2. final, но Map изменяемый!
    
    public User(String name, int age, LocalDate birthDate, 
                List<String> roles, Map<String, String> attributes) {
        this.name = name;  // String — безопасно
        this.age = age;    // примитив — безопасно
        this.birthDate = birthDate;  // LocalDate immutable — безопасно
        
        // Защита для List
        this.roles = Collections.unmodifiableList(
            new ArrayList<>(roles)  // 3. Копируем и оборачиваем
        );
        
        // Защита для Map
        this.attributes = Collections.unmodifiableMap(
            new HashMap<>(attributes)  // 3. Копируем и оборачиваем
        );
    }
    
    // 4. Getters только
    public String getName() {
        return name;  // String — безопасно возвращать
    }
    
    public int getAge() {
        return age;  // примитив — безопасно
    }
    
    public LocalDate getBirthDate() {
        return birthDate;  // LocalDate immutable — безопасно
    }
    
    public List<String> getRoles() {
        return roles;  // Уже unmodifiable, но можно ещё раз обернуть
    }
    
    public Map<String, String> getAttributes() {
        return attributes;  // Уже unmodifiable
    }
    
    // 5. Нет setters
}

// Безопасное использование
List<String> rolesInput = new ArrayList<>();
rolesInput.add("USER");

Map<String, String> attrsInput = new HashMap<>();
attrsInput.put("department", "IT");

User user = new User("Alice", 30, LocalDate.of(1994, 5, 15), rolesInput, attrsInput);

// Попытка менять через исходные объекты
rolesInput.add("ADMIN");  // не повлияет на user
attrsInput.put("admin", "true");  // не повлияет на user

// Попытка менять через getters
user.getRoles().add("ADMIN");  // UnsupportedOperationException!
user.getAttributes().put("admin", "true");  // UnsupportedOperationException!

Ловушка: Изменяемые объекты внутри коллекций

// ❌ ОПАСНО: List из Date (Date изменяемый!)
public final class EventLog {
    private final List<Date> timestamps;  // List неизменяемый, но Date внутри изменяемый!
    
    public EventLog(List<Date> times) {
        this.timestamps = Collections.unmodifiableList(
            new ArrayList<>(times)  // Защитили сам List, но не Date внутри
        );
    }
    
    public List<Date> getTimestamps() {
        return timestamps;
    }
}

// Проблема в использовании
List<Date> dates = new ArrayList<>();
Date d = new Date();
dates.add(d);

EventLog log = new EventLog(dates);

// Можем менять Date!
date.setTime(System.currentTimeMillis() + 1000);
// Это изменит дату в log

Решение: Копировать и содержимое коллекции

// ✅ ПРАВИЛЬНО: Копируем и Date элементы
public final class EventLog {
    private final List<Instant> timestamps;  // Instant — immutable!
    
    public EventLog(List<Date> times) {
        // Преобразуем Date в Instant (immutable)
        this.timestamps = Collections.unmodifiableList(
            times.stream()
                .map(date -> Instant.ofEpochMilli(date.getTime()))
                .collect(Collectors.toList())
        );
    }
    
    public List<Instant> getTimestamps() {
        return timestamps;
    }
}

Правило: Defensive copying

public final class SafeContainer {
    private final List<User> users;
    
    // ВХОДЯЩАЯ ЗАЩИТА: Копируем при получении
    public SafeContainer(List<User> inputUsers) {
        this.users = Collections.unmodifiableList(
            new ArrayList<>(inputUsers)
        );
    }
    
    // ИСХОДЯЩАЯ ЗАЩИТА: Копируем при выдаче
    public List<User> getUsers() {
        // Вариант 1: Возвращаем unmodifiable (самый минималистичный)
        return users;
        
        // Вариант 2: Копируем при каждом обращении (самый параноичный)
        // return new ArrayList<>(users);
        
        // Вариант 3: Unmodifiable + копия
        // return Collections.unmodifiableList(new ArrayList<>(users));
    }
}

Java 14+: Record — автоматическая защита

public record User(
    String name,
    int age,
    List<String> roles
) {
    // Компактный конструктор с автоматической защитой
    public User {
        Objects.requireNonNull(name);
        roles = Collections.unmodifiableList(new ArrayList<>(roles));
    }
    
    // Record автоматически создаёт:
    // - private final поля
    // - getters (name(), age(), roles())
    // - equals, hashCode, toString
    // - НЕ создаёт setters
}

Checklist неизменяемости

✅ Класс final ✅ Все поля final
✅ Нет public конструктора (или стоп валидация) ✅ Нет setters ✅ Для примитивных типов — безопасно ✅ Для String/Instant/UUID/etc (immutable) — безопасно ✅ Для List/Set/Map — Collections.unmodifiableXxx() ✅ Для Date/StringBuilder (mutable) — НЕ использовать или копировать ✅ Оборонительное копирование при конструировании ✅ Unmodifiable обёртка при выдаче mutable коллекций

Заключение

Только final класса + private поля + отсутствие setters недостаточно. Нужно также:

  • Использовать неизменяемые типы (String, LocalDate, Integer)
  • Защищать изменяемые типы (List, Map) через Collections.unmodifiableXxx()
  • Копировать входные данные (defensive copying)
  • Возвращать неизменяемые обёртки для внутренних коллекций
  • Избегать изменяемых типов (Date, StringBuilder) или копировать их
  • Использовать Record (Java 14+) для автоматизации

Полная неизменяемость требует осторожности и внимания к деталям!

Достаточно ли сделать класс final, поля private и не использовать сеттеры для обеспечения неизменяемости объекта после его создания | PrepBro