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

Що таке bounded type parameters

Без обмежень змінна типу може бути замінена абсолютно будь-яким посилальним типом. При накладенні обмеження ви вказуєте, що тип зобов'язаний бути нащадком певного класу або...


🟢 Junior Level

Bounded Type Parameters (обмежені параметри типів) — це механізм дженериків, що дозволяє обмежити діапазон типів, які можна передавати як аргументи дженерика.

Без обмежень змінна типу <T> може бути замінена абсолютно будь-яким посилальним типом. При накладенні обмеження ви вказуєте, що тип зобов’язаний бути нащадком певного класу або реалізовувати певний інтерфейс.

Синтаксис

Для обмеження зверху завжди використовується ключове слово extends (навіть якщо межею виступає інтерфейс!):

// T зобов'язаний бути підтипом Number (Integer, Double, BigDecimal тощо):
public class NumberBox<T extends Number> {
    private T value;

    public void set(T value) { this.value = value; }

    public double getAsDouble() {
        // Завдяки bound компілятор дозволяє викликати методи Number:
        return value.doubleValue();
    }
}

// Використання:
NumberBox<Integer> intBox = new NumberBox<>(); // ✅ Успішно: Integer extends Number
NumberBox<Double> dblBox = new NumberBox<>();  // ✅ Успішно: Double extends Number

// ❌ Помилка компіляції: Type parameter 'java.lang.String' is not within its bound
// NumberBox<String> strBox = new NumberBox<>(); 

🟡 Middle Level

1. Навіщо потрібні обмеження: доступ до методів

У необмеженому дженерику <T> компілятор знає лише те, що $T$ є Object. Ви можете викликати у змінної типу $T$ лише базові методи: equals(), hashCode(), toString().

Обмеження типу відкриває доступ до всіх публічних методів межі (Bound):

// Без обмеження: виклик compareTo() неможливий!
// public static <T> int compare(T a, T b) { return a.compareTo(b); } // ❌ Помилка!

// З обмеженням: компілятор гарантує наявність compareTo()
public static <T extends Comparable<T>> int compare(T a, T b) {
    return a.compareTo(b); // ✅ Безпечний виклик!
}

2. Множинні обмеження (Multiple Bounds)

Один параметр типу може бути обмежений кількома типами через символ & (логічний перетин типів — Intersection Type):

public class Processor<T extends Number & Comparable<T> & Serializable> {
    // T зобов'язаний:
    // 1. Бути підкласом Number
    // 2. Реалізовувати Comparable<T>
    // 3. Реалізовувати Serializable
}

Суворе правило порядку (JLS §4.4):

Якщо серед обмежень присутній клас, він в обов’язковому порядку повинен стояти на першому місці:

// ✅ Вірно: клас іде першим, інтерфейси слідом
public class Valid<T extends Number & Serializable> {}

// ❌ Помилка компіляції: Interface cannot precede a class in bounds
// public class Invalid<T extends Serializable & Number> {}

🔴 Senior Level

Правила стирання при множинних обмеженнях (First Bound Rule)

Специфікація Java (JLS §4.6) вимагає: при стиранні типів змінна $T$ стирається суворо до своєї першої межі (First Bound):

public class Worker<T extends CharSequence & Serializable> {
    private T data;
    public T getData() { return data; }
}

Після компіляції та стирання типів у байткод:

  • Поле data перетворюється на CharSequence (перша межа).
  • В усіх місцях, де об’єкт data використовується в контексті другого інтерфейсу (Serializable), компілятор автоматично генерує перевірочну інструкцію checkcast.

Саме тому клас завжди зобов’язаний бути першою межею: у байткоді JVM поле екземпляра може мати лише один клас-предок (одиночне наслідування класів у Java).

Заборона super для Type Parameters

[!CAUTION] Часта пастка на співбесідах: Чи можна написати class Box<T super Integer>? НІ! Це синтаксична помилка компіляції.

Ключове слово super (нижня межа) застосовне виключно до підстановочних знаків (Wildcards):

List<? super Integer> list; // ✅ Bounded Wildcard дозволений
// class Box<T super Integer> {} // ❌ Помилка компіляції: Type parameter cannot have lower bound

Чому? Параметр типу <T> вводить нову іменовану змінну типу, яка використовується для створення полів та методів. Нижня межа <T super Integer> означала б, що $T$ може бути Integer, Number або Object. Компілятор не міг би гарантувати, які методи доступні у такого об’єкта, а стирання типів призвело б до неможливості вибору типу в байткоді.

Рекурсивні обмеження (Recursive Type Bounds)

Складний патерн, де параметр типу посилається на самого себе:

// Класичний приклад із JDK: java.lang.Enum
public abstract class Enum<E extends Enum<E>> implements Comparable<E> { /* ... */ }

В узагальнених колекціях найкращою практикою є використання Relaxed Recursive Bound (рекомендація Джошуа Блоха):

// Суворе обмеження (ламає наслідування):
public static <T extends Comparable<T>> T max(Collection<T> coll) { /* ... */ }

// Ідіоматичне гнучке обмеження:
public static <T extends Comparable<? super T>> T max(Collection<T> coll) { /* ... */ }

4 Tricky Questions

1. Чи можна оголосити нижню межу (lower bound) для type parameter, наприклад <T super Number>?

Відповідь: Категорично ні.

Специфікація мови Java (JLS §4.4) не допускає використання ключового слова super у декларації параметрів типів (type parameters). Конструкція <T super Number> викличе синтаксичну помилку компіляції.

Ключове слово super зарезервоване виключно для підстановочних знаків (Bounded Wildcards): <? super Number>. Wildcard використовується як анонімний тип аргументу методу при застосуванні принципу PECS (Consumer Super), але створити клас або метод з оголошенням типу <T super ...> неможливо.

2. Чому в оголошенні <T extends ClassA & InterfaceB> клас обов’язково повинен стояти першим, і що станеться, якщо поміняти їх місцями?

Відповідь: Якщо поміняти їх місцями (<T extends InterfaceB & ClassA>), код не скомпільовано з помилкою: «Interface expected here» або «The class must be the first bound».

Це зумовлено двома факторами:

  1. Специфікація стирання типів (JLS §4.6): компілятор завжди стирає тип $T$ до його першої межі (First Bound).
  2. Модель об’єктної пам’яті JVM: у байткоді поле класу повинно мати конкретний тип дескриптора. Оскільки в Java діє одиночне наслідування класів, першим типом зобов’язаний бути клас, щоб стати базовим дескриптором у байткоді.

3. У чому фундаментальна різниця між Bounded Type Parameter (<T extends Number> void method(T val)) та Bounded Wildcard (void method(List<? extends Number> list))?

Відповідь:

  • Bounded Type Parameter (<T extends Number>): оголошує іменовану змінну типу $T$. Вона може використовуватися багаторазово: для зв’язку типів між декількома аргументами (void merge(T a, T b)), для типізації значення, що повертається (T calculate()), а також для інстанціювання локальних змінних всередині методу.
  • Bounded Wildcard (<? extends Number>): являє собою анонімний, невідомий тип. Він служить лише для забезпечення безпечної коваріантності параметрів колекцій (читання даних). Всередині методу не можна оголосити змінну типу ?, і не можна зв’язати два аргументи гарантією, що вони належать до одного й того самого конкретного підтипу.

4. Чому при оголошенні методу пошуку максимуму правильніше писати <T extends Comparable<? super T>>, а не <T extends Comparable<T>>?

Відповідь: Розглянемо ієрархію: базовий клас ScheduledTask implements Comparable<ScheduledTask>, та його нащадок DelayedTask extends ScheduledTask.

Клас DelayedTask не реалізує Comparable<DelayedTask>, він успадкував інтерфейс Comparable<ScheduledTask>. Якщо метод пошуку максимуму оголошений суворо як <T extends Comparable<T>>, компілятор відмовиться приймати List<DelayedTask>, тому що для DelayedTask умова T implements Comparable<T> є хибною (він реалізує Comparable для супертипу).

Конструкція <T extends Comparable<? super T>> розв’язує проблему, дозволяючи порівнювати об’єкти нащадка компаратором будь-якого його суперкласу.


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

Головне про Bounded Type Parameters

  • Синтаксис: <T extends UpperBound>. Ключове слово завжди extends (і для класів, і для інтерфейсів).
  • Сенс: Обмежує тип зверху та відкриває доступ до методів межі.
  • Multiple Bounds: <T extends ClassA & InterfaceB & InterfaceC>. Клас зобов’язаний бути першим!
  • Стирання типів: $T$ стирається до своєї першої межі.
  • Заборона super: <T super Type> заборонений синтаксично. super дозволений лише у wildcards: <? super Type>.
  • PECS у компараторах: Завжди використовуйте <T extends Comparable<? super T>> для підтримки поліморфних ієрархій.

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