Питання 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 і як воно допомагає у створенні незмінних класів]]