Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.2.1. Type Parameters

Параметризованные типы

Материалы

ТипСсылка
Документссылка
Видеоссылка

Type Variables (Переменные типа)

Что такое Type Variable?

Type Variable (переменная типа) - это неквалифицированный идентификатор, используемый как тип в теле класса, интерфейса, метода или конструктора.

Type variable вводится через объявление type parameter (параметра типа) в:

  • Generic классах (§8.1.2)
  • Generic интерфейсах (§9.1.2)
  • Generic методах (§8.4.4)
  • Generic конструкторах (§8.8.4)

Синтаксис Type Parameter

TypeParameter:
    {TypeParameterModifier} TypeIdentifier [TypeBound]

TypeParameterModifier:
    Annotation

TypeBound:
    extends TypeVariable
    extends ClassOrInterfaceType {AdditionalBound}

AdditionalBound:
    & InterfaceType

Объявление Type Parameters

1. Generic классы

// Один type parameter
class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}

// Несколько type parameters
class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

// Использование
Box<String> stringBox = new Box<>();
Pair<String, Integer> pair = new Pair<>("age", 30);

2. Generic интерфейсы

interface Comparable<T> {
    int compareTo(T other);
}

interface Map<K, V> {
    V get(K key);
    V put(K key, V value);
}

3. Generic методы

class Utils {
    // Generic метод
    public static <T> T first(List<T> list) {
        return list.get(0);
    }
    
    // Несколько type parameters
    public static <K, V> Map<K, V> createMap(K[] keys, V[] values) {
        Map<K, V> map = new HashMap<>();
        for (int i = 0; i < keys.length; i++) {
            map.put(keys[i], values[i]);
        }
        return map;
    }
}

// Использование
String first = Utils.<String>first(Arrays.asList("a", "b"));
String first = Utils.first(Arrays.asList("a", "b"));  // Type inference

4. Generic конструкторы

class Foo {
    // Generic конструктор в non-generic классе
    <T> Foo(T arg) {
        // ...
    }
}

// Использование
Foo f = new <String>Foo("hello");
Foo f = new Foo("hello");  // Type inference

Type Bounds (Границы типов)

Unbounded Type Parameter (без ограничений)

class Box<T> {
    private T value;
}

Правило: Если bound не указан, то подразумевается Object.

// Эквивалентно
class Box<T extends Object> {
    private T value;
}

Erasure: T стирается до Object.

Bounded Type Parameter (с ограничениями)

1. Upper Bound (верхняя граница)

// T должен быть Number или его подтип
class NumberBox<T extends Number> {
    private T value;
    
    public double doubleValue() {
        return value.doubleValue();  // Можем вызвать методы Number
    }
}

// Использование
NumberBox<Integer> intBox = new NumberBox<>();    // ✅ OK
NumberBox<Double> doubleBox = new NumberBox<>();  // ✅ OK
NumberBox<String> stringBox = new NumberBox<>();  // ❌ Ошибка компиляции

Erasure: T стирается до Number.

2. Multiple Bounds (множественные границы)

// T должен быть подтипом Number И Comparable
class SortedNumberBox<T extends Number & Comparable<T>> {
    private List<T> values = new ArrayList<>();
    
    public void add(T value) {
        values.add(value);
    }
    
    public T max() {
        T max = values.get(0);
        for (T value : values) {
            if (value.compareTo(max) > 0) {
                max = value;
            }
        }
        return max;
    }
}

// Использование
SortedNumberBox<Integer> box = new SortedNumberBox<>();
box.add(5);
box.add(2);
box.add(8);
Integer max = box.max();  // 8

Синтаксис множественных bounds:

<T extends Class/Interface & Interface1 & Interface2 & ...>

Правила:

  1. ✅ Первый bound может быть класс ИЛИ интерфейс ИЛИ type variable
  2. ❌ Последующие bounds могут быть ТОЛЬКО интерфейсы
  3. ❌ Нельзя указывать два класса в bounds
  4. ❌ Нельзя указывать type variable после первой позиции
// ✅ Правильно
<T extends Number & Comparable<T>>
<T extends Comparable<T> & Serializable>
<T extends List<String> & Cloneable>

// ❌ Неправильно
<T extends Number & String>           // Два класса
<T extends Number & Integer>          // Два класса
<T extends Comparable<T> & Number>    // Класс не первый

Erasure: T стирается до первого типа в bound.

<T extends Number & Comparable<T>>  →  Number (в runtime)
<T extends Comparable<T> & Number>   →  Comparable (в runtime)

Важно: Порядок bounds имеет значение для erasure!

3. Type Variable как Bound

<T, S extends T>  // S должен быть подтипом T
class Pair<T, S extends T> {
    private T first;
    private S second;  // S гарантированно совместим с T
    
    public Pair(T first, S second) {
        this.first = first;
        this.second = second;
    }
}

// Использование
Pair<Number, Integer> pair = new Pair<>(10, 20);  // ✅ Integer extends Number
Pair<Integer, Number> pair = new Pair<>(10, 20);  // ❌ Number не extends Integer

Члены Type Variable

Type variable имеет те же члены (members), что и его intersection type bounds.

Пример

class C {
    public void mCPublic() {}
    protected void mCProtected() {}
    void mCPackage() {}
    private void mCPrivate() {}
}

interface I {
    void mI();
}

class Test {
    <T extends C & I> void test(T t) {
        t.mI();           // ✅ OK - метод интерфейса I
        t.mCPublic();     // ✅ OK - public метод C
        t.mCProtected();  // ✅ OK - protected метод C
        t.mCPackage();    // ✅ OK - package-private метод C (в том же пакете)
        t.mCPrivate();    // ❌ Ошибка - private не доступен
    }
}

Члены type variable T с bound C & I:

  • Все public методы интерфейса I
  • Все accessible методы класса C (кроме private)

Parameterized Types (Параметризованные типы)

Определение

Parameterized type - это класс или интерфейс вида C<T1, T2, ..., Tn>, где:

  • C - имя generic класса или интерфейса
  • <T1, T2, ..., Tn> - список type arguments (аргументов типа)
List<String>              // C = List, T1 = String
Map<String, Integer>      // C = Map, T1 = String, T2 = Integer
Pair<String, String>      // C = Pair, T1 = String, T2 = String

Well-formed Parameterized Type

Parameterized type корректен (well-formed) если:

  1. C - имя generic класса или интерфейса
  2. Количество type arguments = количество type parameters
  3. Каждый type argument соответствует своему bound
// Generic класс с 2 type parameters
class Pair<K, V> { }

// ✅ Корректные parameterized types
Pair<String, Integer>
Pair<Integer, Integer>
Pair<Object, Object>

// ❌ Некорректные parameterized types
Pair<String>              // Недостаточно type arguments
Pair<String, String, String>  // Слишком много type arguments
Pair<int, String>         // Primitive types не допускаются

Проверка bounds при параметризации

class BoundedBox<T extends Number> {
    private T value;
}

// ✅ Корректно - Integer extends Number
BoundedBox<Integer> box1 = new BoundedBox<>();

// ✅ Корректно - Double extends Number
BoundedBox<Double> box2 = new BoundedBox<>();

// ❌ Ошибка компиляции - String не extends Number
BoundedBox<String> box3 = new BoundedBox<>();

Type Arguments (Аргументы типа)

Синтаксис

TypeArguments:
    < TypeArgumentList >

TypeArgumentList:
    TypeArgument {, TypeArgument}

TypeArgument:
    ReferenceType
    Wildcard

Виды Type Arguments

1. Concrete Type (конкретный тип)

List<String>              // Type argument = String
Map<String, Integer>      // Type arguments = String, Integer
Box<List<String>>         // Type argument = List<String> (вложенный)

2. Wildcard (подстановочный знак)

Unbounded Wildcard - ?
List<?> list;  // Список неизвестного типа

Означает: список элементов какого-то типа, но мы не знаем какого.

void printList(List<?> list) {
    for (Object obj : list) {  // Можем читать как Object
        System.out.println(obj);
    }
    // list.add("test");  // ❌ Нельзя добавлять (кроме null)
}
Upper Bounded Wildcard - ? extends T
List<? extends Number> numbers;

Означает: список элементов типа Number или его подтипа.

void sumNumbers(List<? extends Number> numbers) {
    double sum = 0;
    for (Number n : numbers) {  // Можем читать как Number
        sum += n.doubleValue();
    }
    // numbers.add(42);  // ❌ Нельзя добавлять
}

// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);

sumNumbers(ints);     // ✅ OK
sumNumbers(doubles);  // ✅ OK

Правило: Можно читать, нельзя писать (producer).

Lower Bounded Wildcard - ? super T
List<? super Integer> list;

Означает: список элементов типа Integer или его супертипа.

void addIntegers(List<? super Integer> list) {
    list.add(1);      // ✅ Можем добавлять Integer
    list.add(2);      // ✅ Можем добавлять Integer
    
    // Object obj = list.get(0);  // Можем читать только как Object
}

// Использование
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addIntegers(ints);     // ✅ OK
addIntegers(numbers);  // ✅ OK
addIntegers(objects);  // ✅ OK

Правило: Можно писать, можно читать только как Object (consumer).

PECS Principle

PECS = Producer Extends, Consumer Super

// Producer - выдаем элементы (читаем)
<T> void copy(List<? extends T> source, List<? super T> dest) {
    for (T item : source) {  // Читаем из source (producer)
        dest.add(item);      // Пишем в dest (consumer)
    }
}

// Использование
List<Integer> source = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();

copy(source, dest);  // ✅ OK

Provably Distinct Types

Два parameterized type provably distinct (доказуемо различны) если:

  1. Это параметризации разных generic типов

    List<String> и Set<String>  // Провably distinct
    
  2. Любой из их type arguments provably distinct

    List<String> и List<Integer>  // Provably distinct
    List<String> и List<?>        // Provably distinct
    

Зачем нужно?

Для проверки overloading:

// ❌ Ошибка компиляции - после erasure одинаковые сигнатуры
void process(List<String> list) { }
void process(List<Integer> list) { }

// ✅ OK - provably distinct types
void process(List<String> list) { }
void process(Set<String> set) { }

Вложенные Parameterized Types

Nested Generic Classes

class Outer<T> {
    class Inner<S> {
        private T outerValue;
        private S innerValue;
    }
}

// Использование
Outer<String>.Inner<Integer> nested = new Outer<String>().new Inner<Integer>();

Generic Member Class в Non-Generic Outer

class NonGenericOuter {
    class GenericInner<T> {
        private T value;
    }
}

// Использование
NonGenericOuter.GenericInner<String> inner = 
    new NonGenericOuter().new GenericInner<String>();

Non-Generic Inner в Generic Outer

class GenericOuter<T> {
    class NonGenericInner {
        private T outerValue;  // Может использовать T из Outer
    }
}

// Использование
GenericOuter<String>.NonGenericInner inner = 
    new GenericOuter<String>().new NonGenericInner();

Члены Parameterized Types

Правило определения типа члена

Пусть C - generic класс с type parameters A1, ..., An.
Пусть C<T1, ..., Tn> - parameterized type.

Тип члена m в C<T1, ..., Tn>:

T[A1:=T1, A2:=T2, ..., An:=Tn]

Где T - тип члена как объявлено в C.

Примеры

class Box<T> {
    private T value;           // Тип: T
    
    public void set(T v) { }   // Параметр типа: T
    public T get() { }         // Возвращает: T
}

// В Box<String>:
// private String value;
// public void set(String v) { }
// public String get() { }

// В Box<Integer>:
// private Integer value;
// public void set(Integer v) { }
// public Integer get() { }

Static члены

Правило: Static члены generic класса НЕЛЬЗЯ обращаться через parameterized type.

class Box<T> {
    private static int count;  // ✅ OK - static без type parameter
    
    public static int getCount() {
        return count;
    }
    
    // ❌ Нельзя использовать T в static контексте
    // private static T defaultValue;
    // public static T getDefault() { ... }
}

// ❌ Ошибка - нельзя обращаться через parameterized type
Box<String>.getCount();

// ✅ OK - обращение через raw type
Box.getCount();

Почему? Static члены принадлежат классу, а не экземпляру. Type parameter T относится к экземпляру.


Recursive Type Parameters

Определение

Type parameter может ссылаться на себя в своем bound.

class Enum<E extends Enum<E>> {
    // E должен быть подтипом Enum<E>
}

F-bounded Polymorphism

interface Comparable<T> {
    int compareTo(T other);
}

// Класс сравним сам с собой
class Person implements Comparable<Person> {
    private String name;
    
    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
}

Recursive bound:

<T extends Comparable<T>>

Означает: T должен быть сравним с самим собой.

Пример: Generic метод для сортировки

// T должен быть comparable с самим собой
public static <T extends Comparable<T>> T max(List<T> list) {
    if (list.isEmpty()) {
        throw new IllegalArgumentException("Empty list");
    }
    
    T max = list.get(0);
    for (T item : list) {
        if (item.compareTo(max) > 0) {
            max = item;
        }
    }
    return max;
}

// Использование
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer max = max(numbers);  // 5

Type Parameter Naming Conventions

Общепринятые соглашения:

  • T - Type (общий тип)

    class Box<T> { }
    
  • E - Element (элемент коллекции)

    interface List<E> { }
    
  • K - Key (ключ)

    interface Map<K, V> { }
    
  • V - Value (значение)

    interface Map<K, V> { }
    
  • N - Number (числовой тип)

    class Calculator<N extends Number> { }
    
  • S, U, V - дополнительные type parameters

    <T, S, U, V>
    

Правило: Используй заглавные буквы для type parameters.


Ограничения Type Parameters

1. Нельзя создать экземпляр type parameter

class Box<T> {
    // ❌ Нельзя
    public T create() {
        return new T();  // Ошибка компиляции
    }
}

Почему: Type erasure стирает T до Object (или bound).

Workaround: Передавать Class

class Box<T> {
    public T create(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

2. Нельзя создать массив type parameter

class Box<T> {
    // ❌ Нельзя
    private T[] array = new T[10];  // Ошибка компиляции
}

Workaround:

class Box<T> {
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public Box(int size) {
        array = (T[]) new Object[size];  // Unchecked cast
    }
}

3. Нельзя использовать в static контексте

class Box<T> {
    // ❌ Нельзя
    private static T defaultValue;
    
    // ❌ Нельзя
    public static T getDefault() {
        return defaultValue;
    }
}

4. Нельзя использовать instanceof с type parameter

class Box<T> {
    public boolean check(Object obj) {
        // ❌ Нельзя
        if (obj instanceof T) {  // Ошибка компиляции
            return true;
        }
        return false;
    }
}

5. Нельзя перехватывать type parameter exception

// ❌ Нельзя
class GenericException<T extends Exception> extends Exception {
    // ...
}

// ❌ Нельзя
public <T extends Exception> void method() {
    try {
        // ...
    } catch (T e) {  // Ошибка компиляции
        // ...
    }
}

Примеры использования Type Parameters

1. Generic Stack

class Stack<E> {
    private List<E> elements = new ArrayList<>();
    
    public void push(E element) {
        elements.add(element);
    }
    
    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
    
    public E peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.get(elements.size() - 1);
    }
    
    public boolean isEmpty() {
        return elements.isEmpty();
    }
}

// Использование
Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
String top = stack.pop();  // "second"

2. Generic Builder

class Builder<T> {
    private T product;
    
    public Builder(T product) {
        this.product = product;
    }
    
    public Builder<T> with(Consumer<T> consumer) {
        consumer.accept(product);
        return this;
    }
    
    public T build() {
        return product;
    }
}

// Использование
Person person = new Builder<>(new Person())
    .with(p -> p.setName("John"))
    .with(p -> p.setAge(30))
    .build();

3. Generic Repository

interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(ID id);
}

class UserRepository implements Repository<User, Long> {
    @Override
    public User findById(Long id) {
        // implementation
    }
    
    @Override
    public List<User> findAll() {
        // implementation
    }
    
    @Override
    public void save(User user) {
        // implementation
    }
    
    @Override
    public void delete(Long id) {
        // implementation
    }
}

4. Generic Factory

interface Factory<T> {
    T create();
}

class StringFactory implements Factory<String> {
    @Override
    public String create() {
        return new String();
    }
}

// Generic метод с Factory
public static <T> List<T> createList(Factory<T> factory, int count) {
    List<T> list = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        list.add(factory.create());
    }
    return list;
}

Best Practices

✅ DO (рекомендуется):

  1. Используй осмысленные имена для type parameters

    class UserCache<User, UserId> { }  // ✅ Понятно
    class UserCache<T, K> { }          // ❌ Непонятно
    
  2. Указывай bounds когда нужны специфичные операции

    <T extends Comparable<T>> T max(List<T> list)  // ✅
    <T> T max(List<T> list)  // ❌ Нельзя вызвать compareTo
    
  3. Используй wildcards для гибкости API

    void addAll(Collection<? extends E> c)  // ✅ Гибко
    void addAll(Collection<E> c)            // ❌ Слишком строго
    
  4. PECS - Producer Extends, Consumer Super

    <T> void copy(List<? extends T> src, List<? super T> dest)  // ✅
    
  5. Используй type inference где возможно

    List<String> list = new ArrayList<>();  // ✅ Diamond operator
    List<String> list = new ArrayList<String>();  // ❌ Избыточно
    

❌ DON’T (не рекомендуется):

  1. Не используй raw types

    List list = new ArrayList();  // ❌ Raw type
    List<Object> list = new ArrayList<>();  // ✅
    
  2. Не создавай generic массивы

    List<String>[] array = new List<String>[10];  // ❌ Не скомпилируется
    
  3. Не используй type parameters в static контексте

    class Box<T> {
        private static T value;  // ❌
    }
    
  4. Не злоупотребляй wildcards

    // ❌ Слишком сложно
    Map<? extends String, ? super List<? extends Number>> map;
    
    // ✅ Проще и понятнее
    Map<String, List<Number>> map;
    

Заключение

Ключевые моменты:

  1. Type Parameter - это placeholder для типа, который будет указан при использовании
  2. Type Bounds ограничивают допустимые типы и дают доступ к методам
  3. Multiple Bounds позволяют требовать реализацию нескольких интерфейсов
  4. Wildcards (?, ? extends, ? super) добавляют гибкость
  5. Type Erasure стирает информацию о type parameters в runtime
  6. PECS - важный принцип для работы с wildcards

Важные ограничения:

  • ❌ Нельзя создать new T()
  • ❌ Нельзя создать new T[]
  • ❌ Нельзя использовать T в static
  • ❌ Нельзя instanceof T
  • ❌ Нельзя catch T

Паттерны использования:

  • Generic коллекции (List, Map<K,V>)
  • Generic методы для алгоритмов
  • Builder pattern с generics
  • Factory pattern с generics
  • Repository pattern с generics

Type Parameters - мощный инструмент Java для создания type-safe и переиспользуемого кода.!– Добавьте свои заметки здесь –>