Вопрос 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 implements оба
DataBox<Integer> box2 = new DataBox<>(); // ✅ Integer implements оба

Типичные ошибки

  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 инфо
  • ❌ “Можно несколько классов в bounds” — Java single inheritance, максимум один класс

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

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