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

Как создать иммутабельный объект?

2.0 Middle🔥 121 комментариев
#Другое

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

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

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

Создание иммутабельных (неизменяемых) объектов в Java

Что такое иммутабельный объект?

Иммутабельный объект — это объект, состояние которого не может быть изменено после создания. Все попытки модификации создают новый объект.

Примеры встроенных иммутабельных классов:

  • String — строки неизменяемы
  • Integer, Long, Boolean — числовые обёртки
  • LocalDate, LocalTime — классы даты/времени

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

1. Объявить класс как final

public final class ImmutablePoint {
    // Предотвращает наследование
}

2. Все поля должны быть private и final

public final class ImmutablePoint {
    private final int x;
    private final int y;
}

3. Инициализировать поля только в конструкторе

public final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

4. НЕ предоставлять setter методы

// ❌ Неправильно
public void setX(int x) {
    this.x = x; // Нарушает иммутабельность
}

// ✅ Правильно
// Только getter
public int getX() {
    return x;
}

5. Защитить изменяемые поля (если они есть)

public final class ImmutableUser {
    private final String name;
    private final int[] scores; // ❌ Опасно! Массив изменяемый
    
    public ImmutableUser(String name, int[] scores) {
        this.name = name;
        this.scores = scores.clone(); // ✅ Копировать массив
    }
    
    public int[] getScores() {
        return scores.clone(); // ✅ Возвращать копию
    }
}

6. Переопределить equals() и hashCode()

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof ImmutablePoint)) return false;
    ImmutablePoint other = (ImmutablePoint) obj;
    return this.x == other.x && this.y == other.y;
}

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

7. Переопределить toString()

@Override
public String toString() {
    return "Point(" + x + ", " + y + ")";
}

Полный пример: иммутабельный класс точки

import java.util.Objects;

public final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
    
    // Операция, которая возвращает НОВЫЙ объект
    public ImmutablePoint move(int dx, int dy) {
        return new ImmutablePoint(x + dx, y + dy);
    }
    
    public double distanceFrom(ImmutablePoint other) {
        int dx = this.x - other.x;
        int dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof ImmutablePoint)) return false;
        ImmutablePoint other = (ImmutablePoint) obj;
        return this.x == other.x && this.y == other.y;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
    
    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

// Использование
public class Demo {
    public static void main(String[] args) {
        ImmutablePoint p1 = new ImmutablePoint(0, 0);
        
        // Нельзя менять p1
        // p1.setX(5); // ❌ Нет такого метода
        
        // Вместо этого создаём новую точку
        ImmutablePoint p2 = p1.move(5, 3);
        
        System.out.println(p1); // Point(0, 0) — не изменилась
        System.out.println(p2); // Point(5, 3) — новая точка
        
        // Два объекта с одинаковыми координатами равны
        ImmutablePoint p3 = new ImmutablePoint(5, 3);
        System.out.println(p2.equals(p3)); // true
    }
}

// Вывод:
// Point(0, 0)
// Point(5, 3)
// true

Пример: иммутабельный класс с изменяемым полем

import java.util.*;

public final class ImmutableUser {
    private final String name;
    private final List<String> hobbies; // Опасное поле!
    
    // ❌ Неправильная реализация
    public ImmutableUser(String name, List<String> hobbies) {
        this.name = name;
        this.hobbies = hobbies; // Прямое присваивание — ОПАСНО!
    }
    
    public List<String> getHobbies() {
        return hobbies; // ОПАСНО! Возвращаем оригинальный список
    }
}

// Проблема:
List<String> hobbies = new ArrayList<>();
hobbies.add("reading");
ImmutableUser user = new ImmutableUser("John", hobbies);

hobbies.add("gaming"); // Меняем оригинальный список
System.out.println(user.getHobbies()); // [reading, gaming] — ИЗМЕНИЛОСЬ!

✅ Правильная реализация с защитой

import java.util.*;

public final class ImmutableUser {
    private final String name;
    private final List<String> hobbies;
    
    // ✅ Правильная реализация
    public ImmutableUser(String name, List<String> hobbies) {
        this.name = name;
        // Копируем список и делаем его неизменяемым
        this.hobbies = Collections.unmodifiableList(
            new ArrayList<>(hobbies)
        );
    }
    
    public String getName() {
        return name;
    }
    
    public List<String> getHobbies() {
        // Возвращаем неизменяемый список
        return hobbies; // Теперь безопасно, так как READONLY
    }
}

// Использование
List<String> hobbies = new ArrayList<>();
hobbies.add("reading");
ImmutableUser user = new ImmutableUser("John", hobbies);

hobbies.add("gaming");
System.out.println(user.getHobbies()); // [reading] — не изменилось!

// Даже если попытаться менять через getter
user.getHobbies().add("gaming"); // ❌ UnsupportedOperationException

Способ 2: Использовать Java 14+ Records

public record Point(int x, int y) {
    // Автоматически:
    // - final класс
    // - final поля
    // - конструктор
    // - equals(), hashCode(), toString()
    // - getters (x(), y())
}

// Использование
Point p1 = new Point(0, 0);
Point p2 = new Point(5, 3);
System.out.println(p1); // Point[x=0, y=0]
System.out.println(p1.equals(p2)); // false

Способ 3: Использовать @Value аннотацию (Project Lombok)

@Value // Автоматически создаёт иммутабельный класс
public class ImmutablePoint {
    int x;
    int y;
}

// Эквивалентно полной реализации выше

Преимущества иммутабельных объектов

  1. Thread-safety — безопасны в многопоточной среде

    // Нет синхронизации
    Point p = new ImmutablePoint(1, 2);
    // Множество потоков могут читать p одновременно
    
  2. Простота кеширования — можно кешировать

    Map<ImmutablePoint, Integer> cache = new HashMap<>();
    
  3. Простота отладки — состояние не меняется

    ImmutablePoint p = new ImmutablePoint(5, 3);
    // p.x всегда 5, p.y всегда 3
    
  4. Можно использовать в качестве ключей в HashMap

    Map<ImmutablePoint, String> map = new HashMap<>();
    map.put(new ImmutablePoint(1, 2), "start");
    

Недостатки

  1. Больше памяти — каждое изменение создаёт новый объект
  2. Медленнее — при частых изменениях
  3. Боверплейт код — нужно писать копирующие методы

Чеклист для создания иммутабельного класса

  • Класс объявлен как final
  • Все поля private final
  • Конструктор инициализирует все поля
  • Нет setter методов
  • Только getter методы
  • Защита для изменяемых полей (копирование)
  • Переопределены equals() и hashCode()
  • Переопределён toString()

Итог

Иммутабельные объекты — это золотой стандарт для безопасного многопоточного кода. Используй их везде, где возможно, особенно для:

  • Объектов, часто используемых в коллекциях
  • Данных, которые не меняют своё состояние
  • Многопоточных приложений
  • Доменных моделей (Value Objects в DDD)
Как создать иммутабельный объект? | PrepBro