📋 Розділ 20 · Питання #23

Що таке recursive type bound

Звичайне обмеження говорить: «T повинен бути числом». Рекурсивне обмеження > говорить: «T повинен бути не просто порівнюваним з чим за...


🟢 Junior Level

Recursive type bound (рекурсивне обмеження типу) — це оголошення параметра типу дженерика, в якому цей параметр обмежений типом, що залежить від самого цього параметра:

public class Box<T extends Comparable<T>> {
    private final T value;

    public Box(T value) {
        this.value = value;
    }

    public boolean isGreaterThan(T other) {
        return value.compareTo(other) > 0;
    }
}

Проста інтуїція: «Я можу взаємодіяти з такими ж, як я»

Звичайне обмеження <T extends Number> говорить: «T повинен бути числом».
Рекурсивне обмеження <T extends Comparable<T>> говорить: «T повинен бути не просто порівнюваним з чим завгодно, а порівнюваним із самим собою (об’єктами того ж типу T)».

Завдяки цьому:

  • Box<Integer> валідний, оскільки Integer implements Comparable<Integer>.
  • Box<String> валідний, оскільки String implements Comparable<String>.
  • Спроба порівняти Integer зі String буде відхилена компілятором на етапі збирання.

🟡 Middle Level

Класичний приклад із JDK: java.lang.Enum

Найбільш відомий приклад рекурсивного обмеження в Java — це оголошення базового класу перерахувань:

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    private final String name;
    private final int ordinal;

    public final int compareTo(E o) {
        return Integer.compare(this.ordinal, o.ordinal);
    }
}

Коли ми пишемо:

public enum Season { WINTER, SPRING, SUMMER, AUTUMN }

Компілятор перетворює це на клас:

public final class Season extends Enum<Season> { /* ... */ }

Тут тип Season підставляється як параметр E.
Навіщо це потрібно? Метод compareTo(E o) приймає аргумент типу Season. Це на етапі компіляції гарантує, що не можна випадково порівняти значення одного enum з іншим:

enum Status { ACTIVE, INACTIVE }

// ❌ Помилка компіляції: Season не сумісний зі Status
// Season.WINTER.compareTo(Status.ACTIVE); 

Патерн Self-Bounding Types для Fluent Builder

В об’єктно-орієнтованому дизайні рекурсивні межі часто використовуються для реалізації спадкових Fluent API (аналог Curiously Recurring Template Pattern — CRTP):

public abstract class AbstractBuilder<B extends AbstractBuilder<B>> {
    protected String name;

    @SuppressWarnings("unchecked")
    protected B self() {
        return (B) this;
    }

    public B withName(String name) {
        this.name = name;
        return self(); // Повертає точний підклас, а не базовий AbstractBuilder
    }
}

public class UserBuilder extends AbstractBuilder<UserBuilder> {
    private int age;

    public UserBuilder withAge(int age) {
        this.age = age;
        return this;
    }

    public User build() {
        return new User(name, age);
    }
}

// Використання ланцюжка методів базового та дочірнього класів без кастів:
User user = new UserBuilder()
    .withName("Alice") // Повертає UserBuilder!
    .withAge(30)       // Метод доступний у ланцюжку
    .build();

🔴 Senior Level

Проблема наслідування та Relaxed Recursive Bound (? super T)

Розглянемо на перший погляд коректний generic-метод пошуку максимуму:

public static <T extends Comparable<T>> T naiveMax(List<T> list) { /* ... */ }

Уявімо типову ієрархію класів:

public class Animal implements Comparable<Animal> {
    protected int weight;
    @Override
    public int compareTo(Animal other) {
        return Integer.compare(this.weight, other.weight);
    }
}

public class Dog extends Animal {
    // Dog НЕ реалізує Comparable<Dog>!
    // Dog успадковує реалізацію Comparable<Animal> від предка!
}

Що станеться при виклику naiveMax?

List<Dog> dogs = List.of(new Dog(), new Dog());
// ❌ ПОМИЛКА КОМПІЛЯЦІЇ:
// Inference variable T has incompatible bounds:
// equality constraints: Dog
// upper bounds: Comparable<Dog>
// Animal maxDog = naiveMax(dogs);

Клас Dog реалізує Comparable<Animal>, а не Comparable<Dog>. Умова <T extends Comparable<T>> вимагає суворої рівності аргументу інтерфейсу із самим типом.

Рішення: Використання ? super T (Relaxed Bound)

Специфікація та стандартна бібліотека Java формулюють обмеження з підстановочним знаком:

public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)

Тепер компілятор перевіряє: «Чи є T порівнюваним з T або з будь-яким із суперкласів T?». Для Dog суперклас Animal реалізує Comparable<Animal>, і умова виконується ідеально.

Загадка сигнатури Collections.max у JDK

У вихідному коді java.util.Collections метод max оголошений наступним чином:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

Навіщо тут додано надлишковий <T extends Object & ...>?

Це фундаментальний артефакт зворотної бінарної сумісності (Binary Compatibility) з версією Java 1.4:

  1. У Java 1.4 метод Collections.max мав сигнатуру:
    public static Object max(Collection coll)
    
  2. У Java 5 при стиранні типів (Type Erasure) типом стирання generic-параметра стає його перша верхня межа (first bound).
  3. Якби метод оголосили як <T extends Comparable<? super T>>, стертим типом значення, що повертається, став би java.lang.Comparable:
    public static Comparable max(Collection coll) // Зламало б старі class-файли!
    
  4. Додавання Object як першої межі (<T extends Object & Comparable...>) гарантувало, що після стирання дескриптор методу в байткоді залишився в точності рівним (Ljava/util/Collection;)Ljava/lang/Object;. Старий байткод, скомпільований під Java 1.4, продовжив працювати без перекомпільовування!

4 Tricky Questions

1. Чому в Enum<E extends Enum<E>> не можна передати сторонній клас, наприклад class Fake extends Enum<Fake>?

Відповідь:
Компілятор javac явно забороняє пряме наслідування від класу java.lang.Enum у вихідному коді: classes cannot directly extend java.lang.Enum. Створювати підкласи Enum дозволено виключно через ключове слово enum.
Однак сама рекурсивна конструкція дженерика <E extends Enum<E>> на рівні системи типів запобігає змішуванню різних перерахувань в узагальнених методах (наприклад, в EnumSet.noneOf(Class<E>)), гарантуючи, що всі елементи набору належать до одного й того самого enum-типу.

2. Чи захищає патерн Builder<B extends Builder<B>> від передачі некоректного типу нащадка на етапі компіляції?

Відповідь:
Не повністю. Розробник може припуститися помилки і передати «чужий» тип:

public class SubBuilderA extends AbstractBuilder<SubBuilderA> { /* ... */ }
// Помилка програміста: підставив SubBuilderA замість SubBuilderB:
public class SubBuilderB extends AbstractBuilder<SubBuilderA> { /* ... */ }

Компілятор Java пропустить таке оголошення, оскільки SubBuilderA дійсно розширює AbstractBuilder<SubBuilderA>.
У Java не можна декларативно змусити дженерик посилатися суворо на оголошуваний клас без допоміжних перевірок часу виконання (наприклад, self().getClass() == getClass()) або запечатаних ієрархій (sealed classes), що з’явилися в Java 17.

3. На що перетворюється рекурсивна межа <T extends Comparable<T>> після стирання типів?

Відповідь:
Стирання типу (erasure) замінює змінну типу її першою верхньою межею (JLS §4.6).
Стиранням Comparable<T> є сирий тип Comparable. Отже, в байткоді:

  • Поле T value перетворюється на Comparable value.
  • Метод T find(T arg) перетворюється на Comparable find(Comparable arg).
  • У місцях виклику методів T компілятор автоматично вставляє інструкцію checkcast.

4. Чому не можна оголосити рекурсивну межу з super, наприклад <T super MyClass<T>>?

Відповідь:
Ключове слово super у дженериках Java заборонено при оголошенні параметрів типів (type parameters). Воно допустиме виключно в підстановочних знаках (wildcards: <? super T>).
Причина в тому, що тип повинен мати визначену верхню межу, щоб компілятор знав, якими методами володіє об’єкт при зверненні до нього. Нижня межа не дає жодної інформації про доступні методи (зверху залишається тільки Object).


🎯 Шпаргалка для інтерв’ю

  • Що це таке: Параметр типу, обмежений виразом, що містить сам цей параметр (<T extends Comparable<T>>).
  • Навіщо потрібен: Щоб виразити відношення взаємної сумісності типів (наприклад, елемент може порівнюватися лише з об’єктами свого ж типу).
  • Головний приклад із JDK: Enum<E extends Enum<E>> гарантує, що Season.WINTER.compareTo(...) приймає лише Season, захищаючи від порівняння різних enum-ів.
  • Пастка з наслідуванням: Обмеження <T extends Comparable<T>> ламається на підкласах, оскільки підклас успадковує Comparable<Parent>. Рішення: Relaxed Bound <T extends Comparable<? super T>>.
  • Секрет Collections.max: <T extends Object & Comparable<? super T>> містить фіктивний Object &, щоб при стиранні типів значення, що повертається, у байткоді залишалося Object для бінарної сумісності з Java 1.4.
  • Fluent Builder: Використовується для повернення точного типу підкласу з методів батьківського класу (AbstractBuilder<B extends AbstractBuilder<B>>).

Пов’язані теми