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 & ...>
Правила:
- ✅ Первый bound может быть класс ИЛИ интерфейс ИЛИ type variable
- ❌ Последующие bounds могут быть ТОЛЬКО интерфейсы
- ❌ Нельзя указывать два класса в bounds
- ❌ Нельзя указывать 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) если:
- C - имя generic класса или интерфейса
- Количество type arguments = количество type parameters
- Каждый 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 (доказуемо различны) если:
-
Это параметризации разных generic типов
List<String> и Set<String> // Провably distinct -
Любой из их 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 (рекомендуется):
-
Используй осмысленные имена для type parameters
class UserCache<User, UserId> { } // ✅ Понятно class UserCache<T, K> { } // ❌ Непонятно -
Указывай bounds когда нужны специфичные операции
<T extends Comparable<T>> T max(List<T> list) // ✅ <T> T max(List<T> list) // ❌ Нельзя вызвать compareTo -
Используй wildcards для гибкости API
void addAll(Collection<? extends E> c) // ✅ Гибко void addAll(Collection<E> c) // ❌ Слишком строго -
PECS - Producer Extends, Consumer Super
<T> void copy(List<? extends T> src, List<? super T> dest) // ✅ -
Используй type inference где возможно
List<String> list = new ArrayList<>(); // ✅ Diamond operator List<String> list = new ArrayList<String>(); // ❌ Избыточно
❌ DON’T (не рекомендуется):
-
Не используй raw types
List list = new ArrayList(); // ❌ Raw type List<Object> list = new ArrayList<>(); // ✅ -
Не создавай generic массивы
List<String>[] array = new List<String>[10]; // ❌ Не скомпилируется -
Не используй type parameters в static контексте
class Box<T> { private static T value; // ❌ } -
Не злоупотребляй wildcards
// ❌ Слишком сложно Map<? extends String, ? super List<? extends Number>> map; // ✅ Проще и понятнее Map<String, List<Number>> map;
Заключение
Ключевые моменты:
- Type Parameter - это placeholder для типа, который будет указан при использовании
- Type Bounds ограничивают допустимые типы и дают доступ к методам
- Multiple Bounds позволяют требовать реализацию нескольких интерфейсов
- Wildcards (
?,? extends,? super) добавляют гибкость - Type Erasure стирает информацию о type parameters в runtime
- 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 и переиспользуемого кода.!– Добавьте свои заметки здесь –>