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

Що таке bounded type parameters

Ви вказуєте, що тип повинен наслідувати визначений клас або імплементувати інтерфейс.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Bounded type parameters — це спосіб обмежити типи, які можна використовувати з дженериком.

Ви вказуєте, що тип повинен наслідувати визначений клас або імплементувати інтерфейс.

// Без обмеження — будь-який тип
public class Box<T> { }
Box<String> box1 = new Box<>();  // OK
Box<Integer> box2 = new Box<>();  // OK

// З обмеженням — тільки Number та нащадки
public class NumberBox<T extends Number> { }
NumberBox<Integer> box1 = new NumberBox<>();  // ✅ OK
NumberBox<Double> box2 = new NumberBox<>();   // ✅ OK
NumberBox<String> box3 = new NumberBox<>();   // ❌ Помилка!

Синтаксис:

  • T extends Class — тільки цей клас та нащадки
  • T extends Interface — тільки класи, що імплементують інтерфейс

🟡 Middle Level

Як це працює

Один bound:

// Тільки Comparable типи
public class SortedList<T extends Comparable<T>> {
    private List<T> list = new ArrayList<>();

    public void add(T item) {
        list.add(item);
        list.sort(Comparable::compareTo);  // можна — T точно Comparable
    }
}

SortedList<Integer> ints = new SortedList<>();  // ✅ Integer implements Comparable
SortedList<String> strings = new SortedList<>(); // ✅ String implements Comparable
SortedList<Object> objs = new SortedList<>();    // ❌ Object не implements Comparable

Навіщо потрібно:

// Без bound — не можна викликати методи
public class Box<T> {
    public void process(T item) {
        // item.compareTo(other);  // не можна — T може бути будь-яким
    }
}

// З bound — можна викликати методи Number
public class Stats<T extends Number> {
    private List<T> numbers;

    public double average() {
        return numbers.stream()
            .mapToDouble(Number::doubleValue)  // ✅ можна!
            .average().orElse(0);
    }
}

Multiple bounds

Можна вказати кілька обмежень:

// T повинен бути Serializable І Comparable
public class DataBox<T extends Comparable<T> & Serializable> {
    private T value;

    public int compare(T other) {
        return value.compareTo(other);  // ✅
    }

    public byte[] serialize() {
        // ✅ можна серіалізувати — T extends Serializable
        return serializeValue(value);
    }

    private static byte[] serializeValue(Serializable value) {
        // стандартна Java серіалізація
        try (var baos = new java.io.ByteArrayOutputStream();
             var oos = new java.io.ObjectOutputStream(baos)) {
            oos.writeObject(value);
            return baos.toByteArray();
        } catch (java.io.IOException e) {
            throw new RuntimeException(e);
        }
    }
}

DataBox<String> box = new DataBox<>();  // ✅ String імплементує обидва
DataBox<Integer> box2 = new DataBox<>(); // ✅ Integer імплементує обидва

Типові помилки

  1. Неправильний порядок: ```java // ❌ Клас повинен бути першим public class Bad<T extends Serializable & Number> {} // error // JLS вимагає: клас (якщо є) повинен бути першим, бо erasure використовує // перший bound. В Java single inheritance — тільки один клас допустимий.

// ✅ Спочатку клас, потім інтерфейси public class Good<T extends Number & Serializable> {} // OK


2. **Неіснуючий bound:**
```java
// ❌ Integer — final клас, не може бути bound для наслідування
public class Box<T extends Integer> {}  // technically OK але марно

🔴 Senior Level

Internal Implementation

Type erasure з bounds:

public class Stats<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}

// Після erasure: T -> Number (перший bound)
public class Stats {
    private Number value;
    public double doubleValue() { return value.doubleValue(); }
}

Multiple bounds erasure:

public class Box<T extends Comparable<T> & Serializable> { }

// Erasure: T -> перший bound (Comparable)
public class Box {
    private Comparable value;
}

Архітектурні Trade-offs

Обмеження vs unbounded:

Bounded Unbounded
Можна викликати методи Не можна викликати методи
Type-safe Менш type-safe
Обмежена гнучкість Максимальна гнучкість

Edge Cases

1. Recursive bounds:

// T повинен бути Comparable до самого себе
public class Enum<E extends Enum<E>> implements Comparable<E> {
    // Java enum internal
}

// Self-bounding types
public class SelfBounded<T extends SelfBounded<T>> {
    private T self;

    public T set(T self) {
        this.self = self;
        return self;
    }
}

2. Wildcard vs bounded parameter:

// Bounded type parameter — для класів/методів
public class NumberBox<T extends Number> { }

// Bounded wildcard — для параметрів
public void process(List<? extends Number> numbers) { }

// Різниця:
// - <T extends Number> — можна використовувати T в методах
// - <? extends Number> — не можна створювати нові елементи

3. Bridge methods з bounds:

public class Node<T extends Comparable<T>> {
    private T data;
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}

public class DateNode extends Node<Date> {
    @Override
    public Date getData() { return super.getData(); }
    @Override
    public void setData(Date data) { super.setData(data); }

    // Bridge method після erasure:
    // public void setData(Comparable data) { setData((Date) data); }
}

Продуктивність

Bounded types:
- Runtime: Zero overhead
- Compile time: додаткова перевірка
- Type erasure: bound стає типом після компіляції

Bounded vs unbounded:
- однаково в runtime
- Bounded дає більше інформації компілятору
// Після type erasure bound стає compile-time перевіркою.
// JIT не бачить різниці між bounded та unbounded — bytecode однаковий.
// Користь bounds тільки в compile-time type safety.

Production Experience

Generic Repository:

public interface BaseEntity<ID extends Serializable> {
    ID getId();
}

public abstract class JpaRepository<T extends BaseEntity<ID>, ID extends Serializable> {
    private final Class<T> entityType;

    public Optional<T> findById(ID id) {
        // type-safe findById
        return entityManager.find(entityType, id);
    }

    public <S extends T> S save(S entity) {
        if (entity.getId() == null) {
            return persist(entity);
        } else {
            return merge(entity);
        }
    }
}

Builder pattern з bounds:

public abstract class Builder<T, B extends Builder<T, B>> {
    protected abstract B self();

    public B withName(String name) {
        // ... setup
        return self();
    }

    public T build() {
        // ... build
    }
}

public class UserBuilder extends Builder<User, UserBuilder> {
    @Override
    protected UserBuilder self() { return this; }
}

User user = new UserBuilder()
    .withName("John")
    .build();

Best Practices

// ✅ Bounded для виклику методів
public class Stats<T extends Number> {
    public double average() { /* використовувати Number::doubleValue */ }
}

// ✅ Multiple bounds для кількох контрактів
public class DataBox<T extends Comparable<T> & Serializable> {}

// ✅ Recursive bounds для self-types
public class Enum<E extends Enum<E>> {}

// ❌ Занадто строгі bounds
// ❌ Bounds на raw types

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Bounded type parameters: <T extends Number> — тільки Number та нащадки
  • Дозволяють викликати методи bound-типу: value.doubleValue() для T extends Number
  • Multiple bounds: <T extends Comparable<T> & Serializable> — клас перший, потім інтерфейси
  • Type erasure використовує перший bound: T extends Number & Serializable -> erasure = Number
  • Recursive bounds: <E extends Enum<E>> — тип порівнюється з самим собою
  • JLS вимагає: клас (якщо є) повинен бути першим в списку bounds

Часті уточнюючі запитання:

  • Навіщо потрібні bounded types? — Щоб викликати методи bound-типу всередині generic класу
  • Чому клас повинен бути першим в multiple bounds? — Erasure використовує перший bound, single inheritance
  • Що таке recursive bound?T extends Comparable<T> — тип порівнюється з таким самим типом
  • Relaxed recursive bound?T extends Comparable<? super T> — дозволяє нащадкам теж працювати

Червоні прапорці (НЕ говорити):

  • ❌ “Можна поставити інтерфейс перед класом в bounds” — JLS вимагає клас першим
  • ❌ “Multiple bounds дають runtime overhead” — Zero overhead, bound тільки compile-time
  • ❌ “Bounded і unbounded однакові в runtime” — Однакові після erasure, але bounds дають більше compile-time info
  • ❌ “Можна кілька класів в bounds” — Java single inheritance, максимум один клас

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

  • [[11. Що таке дженерики (Generics) в Java]]
  • [[13. Що таке type erasure (стирання типів)]]
  • [[16. У чому різниця між <? extends T> і <? super T>]]
  • [[23. Що таке recursive type bound]]
  • [[26. Чи можна використовувати кілька обмежень (bounds) для одного параметра типу]]