Вопрос 15 · Раздел 13

Можно ли наследоваться от иммутабельного класса?

Технически — да, если класс не помечен как final. Но это плохая идея, потому что подкласс может добавить изменяемые поля, и тогда код, ожидающий неизменяемый объект, получит мут...

Версии по языкам: English Russian Ukrainian

Junior Level

Технически — да, если класс не помечен как final. Но это плохая идея, потому что подкласс может добавить изменяемые поля, и тогда код, ожидающий неизменяемый объект, получит мутабельный.

Пример проблемы

// "Иммутабельный" класс, но без final
public class ImmutableBase {
    private final String value;
    public ImmutableBase(String v) { this.value = v; }
    public String getValue() { return value; }
}

// Подкласс добавляет мутабельность
public class MutableChild extends ImmutableBase {
    private String extra; // мутабельное поле
    public void setExtra(String e) { this.extra = e; }
}

Решение

Всегда объявляйте иммутабельные классы как final:

public final class ImmutablePoint { ... } // наследование невозможно

Middle Level

Почему наследование ломает иммутабельность

  1. Новые мутабельные поля — подкласс может добавить private String extra с сеттером
  2. Переопределение методов — подкласс может переопределить геттеры, возвращая другие данные
  3. Нестабильный hashCode — иммутабельные объекты часто кэшируют hashCode; подкласс с мутабельными полями делает этот кэш невалидным.

Альтернативы наследованию

1. Композиция вместо наследования

public final class ExtendedPoint {
    private final ImmutablePoint point;
    private final String label;
    // ...
}

2. Приватные конструкторы + статические фабрики

public final class ImmutableObject {
    private final String data;
    private ImmutableObject(String data) { this.data = data; }
    public static ImmutableObject of(String data) { return new ImmutableObject(data); }
}

Примеры из JDK

Все фундаментальные иммутабельные классы — final: String, Integer, BigDecimal, LocalDate.


Senior Level

Полиморфизм как угроза

Если метод ожидает ImmutableBase и полагается на его неизменность (например, кэширует результат на основе getValue()), подстановка MutableChild может привести к:

  • Нарушению инвариантов — данные изменятся после проверки
  • Security bypass — подкласс вернёт другие данные после валидации
  • Потере потокобезопасности — подкласс обращается к не-final полям без синхронизации

Sealed Classes (preview в Java 15, stable в Java 17)

Если нужно ограниченное наследование:

public sealed class Shape permits Circle, Rectangle {
    // Оба permits-класса тоже должны быть иммутабельными
}

Резюме для Senior

  • В большинстве случаев наследование от иммутабельного класса — антипаттерн. Исключение: sealed class hierarchy (Java 17+), где все потомки также иммутабельны.
  • Всегда помечайте иммутабельные классы как final
  • Иммутабельность — это не только final поля, но и запрет на изменение логики через полиморфизм
  • Используйте Sealed classes для контролируемого наследования в Java 17+

🎯 Шпаргалка для интервью

Обязательно знать:

  • Технически можно (если не final), но это антипаттерн — подкласс добавит мутабельность
  • Подкласс может: добавить мутабельные поля, переопределить геттеры, сделать hashCode нестабильным
  • Решение: всегда final class для иммутабельных классов
  • Альтернативы: композиция вместо наследования, sealed classes (Java 17+)
  • Все фундаментальные иммутабельные классы JDK — final: String, Integer, BigDecimal, LocalDate
  • Sealed classes позволяют ограниченное наследование с контролем потомков

Частые уточняющие вопросы:

  • Почему наследование ломает иммутабельность? — Подкласс добавляет мутабельные поля или переопределяет геттеры
  • Что если нужно расширить функционал? — Композиция: обернуть иммутабельный объект в новый final класс
  • Sealed classes vs final? — Sealed разрешает конкретных наследников, final запрещает всех
  • Полиморфизм как угроза? — Метод полагается на неизменность, подкласс подменяет данные после проверки

Красные флаги (НЕ говорить):

  • «Наследование от иммутабельного класса — нормально» — это антипаттерн
  • «Подкласс не может изменить final поля» — верно, но может переопределить геттеры
  • «Sealed classes = финал» — sealed разрешает наследование, но ограниченное
  • «Можно добавить mutable поле в подкласс» — это разрушает контракт иммутабельности

Связанные темы:

  • [[16. Почему иммутабельный класс должен быть final]]
  • [[17. Что произойдёт, если переопределить геттер в подклассе иммутабельного класса]]
  • [[3. Как создать иммутабельный класс в Java]]
  • [[7. Что такое ключевое слово final и как оно помогает в создании иммутабельных классов]]