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

Как сделать Immutable класс

1.3 Junior🔥 181 комментариев
#Основы Java

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

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

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

Как создать Immutable (неизменяемый) класс в Java

Immutable класс — это класс, экземпляры которого не могут быть изменены после создания. Это критически важно для многопоточного программирования, так как immutable объекты потокобезопасны по определению и не требуют синхронизации.

Правила создания Immutable класса

Для создания истинно неизменяемого класса нужно следовать этим правилам:

  1. Поля должны быть private и final
  2. Нет setter методов
  3. Класс должен быть final (чтобы нельзя было создать mutable подкласс)
  4. Конструктор должен быть единственным способом инициализации
  5. Если поля содержат mutable объекты, нужно делать defensive copy
  6. Методы должны возвращать новые объекты, а не модифицировать существующие

Базовый пример: Immutable User

// ✅ ПРАВИЛЬНО: Immutable класс
public final class User {
  private final long id;
  private final String name;
  private final String email;
  private final int age;
  
  // Единственный конструктор
  public User(long id, String name, String email, int age) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.age = age;
  }
  
  // Только getter методы, БЕЗ setters
  public long getId() {
    return id;
  }
  
  public String getName() {
    return name;
  }
  
  public String getEmail() {
    return email;
  }
  
  public int getAge() {
    return age;
  }
  
  // ВАЖНО: реализовать equals, hashCode, toString
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    User user = (User) o;
    return id == user.id && 
           age == user.age && 
           Objects.equals(name, user.name) && 
           Objects.equals(email, user.email);
  }
  
  @Override
  public int hashCode() {
    return Objects.hash(id, name, email, age);
  }
  
  @Override
  public String toString() {
    return "User{" + "id=" + id + ", name='" + name + "'" + 
           ", email='" + email + "'" + ", age=" + age + "}";
  }
}

// Использование
User user = new User(1L, "John Doe", "john@example.com", 30);
System.out.println(user); // User{id=1, name='John Doe', email='john@example.com', age=30}

// Невозможно изменить поля (нет setters)
// user.setName("Jane"); // Compile error!

Сложный случай: Immutable с mutable полями

Если класс содержит ссылки на mutable объекты (List, Map, Date), нужно делать defensive copy:

// ❌ НЕПРАВИЛЬНО: содержит ссылку на mutable List
public final class BadTeam {
  private final String name;
  private final List<String> members; // Ссылка на mutable объект!
  
  public BadTeam(String name, List<String> members) {
    this.name = name;
    this.members = members; // ПРОБЛЕМА: внешний код может изменить список
  }
  
  public List<String> getMembers() {
    return members; // ПРОБЛЕМА: возвращаем ссылку, которую можно изменить
  }
}

// Использование
List<String> teamMembers = new ArrayList<>(Arrays.asList("Alice", "Bob"));
BadTeam team = new BadTeam("Developers", teamMembers);
teamMembers.add("Charlie"); // Изменяем через внешнюю ссылку!

// ✅ ПРАВИЛЬНО: делаем defensive copy
public final class GoodTeam {
  private final String name;
  private final List<String> members; // Внутренняя ссылка
  
  // Defensive copy в конструкторе
  public GoodTeam(String name, List<String> members) {
    this.name = Objects.requireNonNull(name, "Name cannot be null");
    // Создаём неизменяемую копию
    this.members = Collections.unmodifiableList(new ArrayList<>(members));
  }
  
  // Defensive copy при возврате
  public List<String> getMembers() {
    // Возвращаем неизменяемый список
    return Collections.unmodifiableList(members);
  }
  
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof GoodTeam)) return false;
    GoodTeam goodTeam = (GoodTeam) o;
    return Objects.equals(name, goodTeam.name) && 
           Objects.equals(members, goodTeam.members);
  }
  
  @Override
  public int hashCode() {
    return Objects.hash(name, members);
  }
}

// Использование
List<String> teamMembers = new ArrayList<>(Arrays.asList("Alice", "Bob"));
GoodTeam team = new GoodTeam("Developers", teamMembers);
teamMembers.add("Charlie"); // Не влияет на team!
System.out.println(team.getMembers()); // [Alice, Bob]

Пример с Date (mutable объект)

// ❌ НЕПРАВИЛЬНО
public final class BadEvent {
  private final String name;
  private final Date date; // Date is mutable!
  
  public BadEvent(String name, Date date) {
    this.name = name;
    this.date = date; // Проблема
  }
  
  public Date getDate() {
    return date; // Проблема: можно изменить
  }
}

// ✅ ПРАВИЛЬНО
public final class GoodEvent {
  private final String name;
  private final LocalDateTime dateTime; // LocalDateTime immutable
  
  public GoodEvent(String name, LocalDateTime dateTime) {
    this.name = Objects.requireNonNull(name);
    this.dateTime = Objects.requireNonNull(dateTime);
  }
  
  // Или с Date (делаем defensive copy)
  public GoodEvent(String name, Date date) {
    this.name = Objects.requireNonNull(name);
    this.dateTime = new java.time.Instant(date.getTime())
      .atZone(java.time.ZoneId.systemDefault())
      .toLocalDateTime();
  }
  
  public LocalDateTime getDateTime() {
    return dateTime; // Безопасно возвращать (immutable)
  }
}

Builder паттерн для создания Immutable объектов

Для сложных immutable объектов удобно использовать Builder:

public final class Person {
  private final String firstName;
  private final String lastName;
  private final int age;
  private final String email;
  private final String phone;
  private final Address address;
  
  private Person(Builder builder) {
    this.firstName = builder.firstName;
    this.lastName = builder.lastName;
    this.age = builder.age;
    this.email = builder.email;
    this.phone = builder.phone;
    this.address = builder.address;
  }
  
  // Builder class
  public static class Builder {
    private String firstName;
    private String lastName;
    private int age;
    private String email;
    private String phone;
    private Address address;
    
    public Builder firstName(String firstName) {
      this.firstName = firstName;
      return this;
    }
    
    public Builder lastName(String lastName) {
      this.lastName = lastName;
      return this;
    }
    
    public Builder age(int age) {
      this.age = age;
      return this;
    }
    
    public Builder email(String email) {
      this.email = email;
      return this;
    }
    
    public Builder phone(String phone) {
      this.phone = phone;
      return this;
    }
    
    public Builder address(Address address) {
      this.address = address;
      return this;
    }
    
    // Validation in build()
    public Person build() {
      if (firstName == null || firstName.isBlank()) {
        throw new IllegalArgumentException("First name is required");
      }
      if (lastName == null || lastName.isBlank()) {
        throw new IllegalArgumentException("Last name is required");
      }
      return new Person(this);
    }
  }
  
  // Getters only
  public String getFirstName() { return firstName; }
  public String getLastName() { return lastName; }
  public int getAge() { return age; }
  public String getEmail() { return email; }
  public String getPhone() { return phone; }
  public Address getAddress() { return address; }
  
  @Override
  public String toString() {
    return "Person{" + "firstName='" + firstName + "'" + 
           ", lastName='" + lastName + "'" + ", age=" + age + "}";
  }
}

// Использование Builder
Person person = new Person.Builder()
  .firstName("John")
  .lastName("Doe")
  .age(30)
  .email("john@example.com")
  .phone("+1234567890")
  .build();

Records как готовое решение (Java 16+)

Java Records — это встроенный способ создания immutable классов:

// ✅ ИДЕАЛЬНО: Records
public record User(long id, String name, String email, int age) {}

// Records автоматически:
// ✓ final поля
// ✓ final класс
// ✓ implements equals(), hashCode(), toString()
// ✓ Конструктор и getters

// Использование
User user = new User(1L, "John Doe", "john@example.com", 30);
System.out.println(user); // User[id=1, name=John Doe, email=john@example.com, age=30]

// С validation
public record Person(String name, int age) {
  public Person {
    if (age < 0) {
      throw new IllegalArgumentException("Age cannot be negative");
    }
  }
}

Сравнение подходов

ПодходBoilerplateПростотаСовременность
Обычный классМногоПростая2000s
Builder паттернСреднееСложная2006+
Lombok @ValueМинимумПростая2010s
RecordsМинимумОчень простая2021+

Преимущества Immutable классов

Thread-safe по определению (не нужны locks) ✅ Можно кэшировать (всегда одинаковые) ✅ Безопасны в Map/Set (hashCode не меняется) ✅ Легче тестировать (predictable behavior) ✅ Защита от побочных эффектов (defensive copy) ✅ Functional programming friendly

Итоги

Для создания immutable класса:

  1. Сделай класс final
  2. Поля private final
  3. Нет setters
  4. Делай defensive copies для mutable объектов
  5. Используй Records (Java 16+) если возможно
  6. Реализуй equals(), hashCode(), toString()
Как сделать Immutable класс | PrepBro