2.2.2. Wildcards
? extends, ? super, PECS
Материалы
Wildcards - Подстановочные знаки
Что такое Wildcards?
Wildcard (подстановочный знак) - это специальный тип-аргумент в Java дженериках, обозначаемый символом ?.
Wildcards используются когда:
- Нужна частичная информация о типе
- Требуется гибкость в работе с параметризованными типами
- Не нужна полная информация о конкретном типе
Синтаксис
Wildcard:
{Annotation} ? [WildcardBounds]
WildcardBounds:
extends ReferenceType
super ReferenceType
Виды Wildcards
1. Unbounded Wildcard - ?
Определение: Wildcard без ограничений.
List<?> list;
Означает: Список элементов какого-то типа, но мы не знаем какого.
Когда использовать?
- Когда нужно работать с коллекцией, но тип элементов не важен
- Когда используются только методы Object
- Когда пишешь универсальный код
Пример
// Печать любой коллекции
public static void printCollection(Collection<?> c) {
for (Object element : c) { // Читаем как Object
System.out.println(element);
}
}
// Использование
Collection<String> strings = Arrays.asList("a", "b", "c");
Collection<Integer> integers = Arrays.asList(1, 2, 3);
printCollection(strings); // ✅ OK
printCollection(integers); // ✅ OK
Ограничения Unbounded Wildcard
List<?> list = new ArrayList<String>();
// ✅ Можно читать как Object
Object obj = list.get(0);
// ❌ Нельзя добавлять (кроме null)
list.add("test"); // Ошибка компиляции!
list.add(123); // Ошибка компиляции!
list.add(null); // ✅ OK - null можно
// ✅ Можно вызывать методы без параметров типа
int size = list.size();
boolean isEmpty = list.isEmpty();
list.clear();
Почему нельзя добавлять?
List<?> list = new ArrayList<String>(); // На самом деле List<String>
// Если бы можно было:
list.add(123); // Добавили Integer в List<String>! Нарушение type safety
? vs Object
// ❌ Неправильно - слишком строго
void printList(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List<String> strings = Arrays.asList("a", "b");
printList(strings); // ❌ Ошибка! List<String> не является List<Object>
// ✅ Правильно - гибко
void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
printList(strings); // ✅ OK
Важно: List<String> НЕ является подтипом List<Object>, но ЯВЛЯЕТСЯ подтипом List<?>.
2. Upper Bounded Wildcard - ? extends T
Определение: Wildcard с верхней границей.
List<? extends Number> numbers;
Означает: Список элементов типа Number или его подтипа (Integer, Double, Float, и т.д.).
Иерархия подтипов
Number
|
┌──────┼──────┐
| | |
Integer Double Float
List<? extends Number> numbers;
// ✅ Все это можно присвоить
numbers = new ArrayList<Number>();
numbers = new ArrayList<Integer>();
numbers = new ArrayList<Double>();
numbers = new ArrayList<Float>();
// ❌ Нельзя - String не extends Number
numbers = new ArrayList<String>();
Чтение и запись
List<? extends Number> numbers = new ArrayList<Integer>();
// ✅ Можно читать как Number
Number n = numbers.get(0);
double d = numbers.get(0).doubleValue();
// ❌ Нельзя добавлять (кроме null)
numbers.add(Integer.valueOf(1)); // Ошибка компиляции!
numbers.add(Double.valueOf(2.0)); // Ошибка компиляции!
numbers.add(new Object()); // Ошибка компиляции!
numbers.add(null); // ✅ OK - только null
Почему нельзя добавлять?
List<? extends Number> numbers = new ArrayList<Integer>(); // На самом деле List<Integer>
// Если бы можно было:
numbers.add(Double.valueOf(3.14)); // Добавили Double в List<Integer>! Ошибка!
Компилятор не знает точный тип, поэтому запрещает добавление чего-либо (кроме null).
Пример: Суммирование чисел
public static double sum(List<? extends Number> numbers) {
double sum = 0.0;
for (Number n : numbers) { // Читаем как Number
sum += n.doubleValue();
}
return sum;
}
// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
List<Long> longs = Arrays.asList(10L, 20L, 30L);
sum(ints); // ✅ 6.0
sum(doubles); // ✅ 7.5
sum(longs); // ✅ 60.0
Get & Put Principle
? extends T - это Producer (производитель):
- ✅ Get - можно читать (produces T)
- ❌ Put - нельзя писать (кроме null)
3. Lower Bounded Wildcard - ? super T
Определение: Wildcard с нижней границей.
List<? super Integer> list;
Означает: Список элементов типа Integer или его супертипа (Number, Object).
Иерархия супертипов
Object
|
Number
|
Integer
List<? super Integer> list;
// ✅ Все это можно присвоить
list = new ArrayList<Integer>();
list = new ArrayList<Number>();
list = new ArrayList<Object>();
// ❌ Нельзя - Double не является супертипом Integer
list = new ArrayList<Double>();
Чтение и запись
List<? super Integer> list = new ArrayList<Number>();
// ✅ Можно добавлять Integer и его подтипы
list.add(Integer.valueOf(1)); // ✅ OK
list.add(Integer.valueOf(2)); // ✅ OK
list.add(null); // ✅ OK
// ❌ Нельзя добавлять супертипы Integer
list.add(new Number()); // Ошибка компиляции!
list.add(new Object()); // Ошибка компиляции!
// ⚠️ Можно читать только как Object
Object obj = list.get(0); // ✅ OK
Integer i = list.get(0); // ❌ Ошибка компиляции!
Number n = list.get(0); // ❌ Ошибка компиляции!
Почему можно добавлять Integer?
List<? super Integer> list = new ArrayList<Number>(); // Может быть Number, Object
// Integer можно добавить в любой из супертипов
list.add(Integer.valueOf(1)); // ✅ OK - Integer -> Number
list.add(Integer.valueOf(2)); // ✅ OK - Integer -> Object
Почему нельзя читать как Integer?
List<? super Integer> list = new ArrayList<Object>(); // На самом деле List<Object>
// Если бы можно было:
Integer i = list.get(0); // get() вернет Object, не Integer! Ошибка type safety
Пример: Добавление элементов
public static void addIntegers(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(i); // ✅ Можем добавлять Integer
}
}
// Использование
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(integers, 3); // ✅ OK - [0, 1, 2]
addIntegers(numbers, 3); // ✅ OK - [0, 1, 2]
addIntegers(objects, 3); // ✅ OK - [0, 1, 2]
Get & Put Principle
? super T - это Consumer (потребитель):
- ❌ Get - можно читать только как Object
- ✅ Put - можно писать T (consumes T)
PECS Principle
Producer Extends, Consumer Super
PECS - мнемоническое правило для выбора между extends и super.
Producer → ? extends T → Читаем из коллекции (Get)
Consumer → ? super T → Пишем в коллекцию (Put)
Визуализация
? extends T
(Producer)
↓
[Collection] ----→ (Get) → Your Code
? super T
(Consumer)
↑
Your Code → (Put) → [Collection]
Классический пример: метод copy
// Копирование из source в destination
public static <T> void copy(
List<? extends T> source, // Producer - читаем из source
List<? super T> destination // Consumer - пишем в destination
) {
for (T item : source) { // Get from producer
destination.add(item); // Put to consumer
}
}
Использование:
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(integers, numbers); // ✅ OK
// integers is producer (? extends T)
// numbers is consumer (? super T)
System.out.println(numbers); // [1, 2, 3]
Почему это работает?
// source: List<? extends T> где T = Number
List<Integer> integers; // ✅ Integer extends Number
// destination: List<? super T> где T = Number
List<Number> numbers; // ✅ Number super Number
List<Object> objects; // ✅ Object super Number
Пример из стандартной библиотеки
Collections.copy()
public static <T> void copy(
List<? super T> dest, // Consumer
List<? extends T> src // Producer
) {
// implementation
}
Collection.addAll()
interface Collection<E> {
boolean addAll(Collection<? extends E> c); // Producer
// ↑
// Коллекция-источник (producer)
}
Почему ? extends E, а не просто E?
List<Number> numbers = new ArrayList<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
// С ? extends E
numbers.addAll(integers); // ✅ OK - гибко
// Если было бы просто E
// numbers.addAll(integers); ❌ Не скомпилировалось бы
Сравнение типов Wildcards
| Wildcard | Чтение | Запись | Use Case |
|---|---|---|---|
? | ✅ Object | ❌ (только null) | Тип не важен, используем методы Object |
? extends T | ✅ T | ❌ (только null) | Producer - читаем элементы как T |
? super T | ⚠️ Object | ✅ T | Consumer - пишем элементы типа T |
T | ✅ T | ✅ T | Полный контроль над типом |
Когда использовать каждый?
// 1. Тип не важен - используй ?
void printSize(Collection<?> c) {
System.out.println(c.size());
}
// 2. Читаем из коллекции - используй ? extends T
double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
// 3. Пишем в коллекцию - используй ? super T
void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 4. И читаем И пишем - используй конкретный тип T
<T> void sort(List<T> list, Comparator<? super T> c) {
// Нужен полный доступ к list
}
Wildcard Capture
Что такое Wildcard Capture?
Wildcard Capture - это процесс “захвата” wildcard в type variable.
Проблема
public static void swap(List<?> list, int i, int j) {
// ❌ Ошибка компиляции
Object temp = list.get(i);
list.set(i, list.get(j)); // Нельзя! Тип ? несовместим с Object
list.set(j, temp);
}
Проблема: ? - это “какой-то неизвестный тип”, а не Object.
Решение: Helper метод с type parameter
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // Делегируем к helper
}
// Helper метод захватывает wildcard в type parameter T
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i); // ✅ OK
list.set(i, list.get(j)); // ✅ OK
list.set(j, temp); // ✅ OK
}
Как работает:
List<?>передается вswapHelper- Компилятор выводит T = capture of ?
- В
swapHelperтип известен как T - Теперь можно читать и писать
Пример: Reverse list
public static void reverse(List<?> list) {
reverseHelper(list);
}
private static <T> void reverseHelper(List<T> list) {
int size = list.size();
for (int i = 0; i < size / 2; i++) {
T temp = list.get(i);
list.set(i, list.get(size - 1 - i));
list.set(size - 1 - i, temp);
}
}
// Использование
List<String> strings = new ArrayList<>(Arrays.asList("a", "b", "c"));
reverse(strings); // ["c", "b", "a"]
Ограничения Wildcards
1. Нельзя создать экземпляр wildcard
// ❌ Нельзя
List<?> list = new ArrayList<?>(); // Ошибка компиляции
// ✅ Можно
List<?> list = new ArrayList<String>();
List<?> list = new ArrayList<>();
2. Нельзя использовать wildcard в new
// ❌ Нельзя
class Box<T> {
public static Box<?> create() {
return new Box<?>(); // Ошибка компиляции
}
}
// ✅ Можно
class Box<T> {
public static Box<?> create() {
return new Box<Object>(); // OK
}
}
3. Нельзя использовать multiple wildcards в type parameter
// ❌ Нельзя
class Pair<?, ?> { } // Ошибка компиляции
// ✅ Можно
class Pair<T, S> { }
Pair<?, ?> pair; // OK - wildcard в использовании, не в объявлении
4. Wildcard не может иметь lower bound в type parameter
// ❌ Нельзя
<T super Number> void method() { } // Ошибка компиляции
// ✅ Можно - только в использовании
void method(List<? super Number> list) { }
5. Нельзя использовать wildcard в extends для классов
// ❌ Нельзя
class MyList extends ArrayList<?> { } // Ошибка компиляции
// ✅ Можно
class MyList<T> extends ArrayList<T> { }
Взаимодействие Wildcards
Вложенные Wildcards
// List списков неизвестного типа
List<List<?>> lists = new ArrayList<>();
lists.add(new ArrayList<String>()); // ✅ OK
lists.add(new ArrayList<Integer>()); // ✅ OK
List<?> firstList = lists.get(0); // ✅ OK
Object obj = firstList.get(0); // ✅ OK
Wildcards в Map
// Map с wildcard в value
Map<String, ? extends Number> map = new HashMap<>();
// Можем читать values как Number
Number n = map.get("key");
// Map с wildcard в key
Map<? extends String, Integer> map = new HashMap<>();
// Можем читать keys как String
// Map с wildcards в обоих
Map<? extends String, ? extends Number> map = new HashMap<>();
Типичные паттерны использования
1. Чтение из коллекций разных типов
// Хотим работать с любым списком чисел
public static void printNumbers(List<? extends Number> numbers) {
for (Number n : numbers) {
System.out.println(n);
}
}
// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
printNumbers(ints); // ✅
printNumbers(doubles); // ✅
2. Запись в коллекции разных типов
// Хотим добавить элементы в коллекцию-супертип
public static void fillWithZeros(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(0);
}
}
// Использование
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
fillWithZeros(ints, 5); // ✅
fillWithZeros(numbers, 5); // ✅
fillWithZeros(objects, 5); // ✅
3. Поиск максимума
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException();
}
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
Разбор сигнатуры:
List<? extends T>- список элементов T или подтипа T (producer)T extends Comparable<? super T>- T сравним с собой или супертипом
4. Конвертация коллекций
public static <T, S extends T> List<T> convert(List<S> source) {
List<T> result = new ArrayList<>();
for (S item : source) {
result.add(item); // S автоматически преобразуется к T
}
return result;
}
// Использование
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = convert(integers); // Integer -> Number
5. Union множеств
public static <T> Set<T> union(
Set<? extends T> set1,
Set<? extends T> set2
) {
Set<T> result = new HashSet<>(set1); // Копируем set1
result.addAll(set2); // Добавляем set2
return result;
}
// Использование
Set<Integer> ints = Set.of(1, 2, 3);
Set<Integer> moreInts = Set.of(3, 4, 5);
Set<Integer> allInts = union(ints, moreInts); // {1, 2, 3, 4, 5}
// Можем объединять подтипы
Set<Integer> integers = Set.of(1, 2);
Set<Double> doubles = Set.of(3.0, 4.0);
Set<Number> numbers = union(integers, doubles); // {1, 2, 3.0, 4.0}
Частые ошибки и решения
Ошибка 1: Попытка записи в ? extends
// ❌ Неправильно
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(Integer.valueOf(1)); // Ошибка компиляции!
// ✅ Правильно - используй конкретный тип
List<Integer> numbers = new ArrayList<>();
numbers.add(1); // OK
Ошибка 2: Чтение из ? super не как Object
// ❌ Неправильно
List<? super Integer> list = new ArrayList<Number>();
Integer i = list.get(0); // Ошибка компиляции!
// ✅ Правильно - читай как Object
Object obj = list.get(0);
if (obj instanceof Integer) {
Integer i = (Integer) obj;
}
Ошибка 3: Неправильный выбор между extends и super
// ❌ Неправильно - используем super когда нужно extends
public double sum(List<? super Number> numbers) {
double sum = 0;
for (Number n : numbers) { // Ошибка! Нельзя читать как Number
sum += n.doubleValue();
}
return sum;
}
// ✅ Правильно - используем extends для чтения
public double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) { // OK
sum += n.doubleValue();
}
return sum;
}
Ошибка 4: Использование wildcard вместо type parameter
// ❌ Плохой стиль - wildcard используется только один раз
public static void addAll(Collection<? extends E> c) {
// ...
}
// ✅ Лучше использовать type parameter
public static <T extends E> void addAll(Collection<T> c) {
// ...
}
Правило: Если type parameter используется только один раз и нет взаимосвязи между типами, используй wildcard.
Эквивалентность Wildcards
? эквивалентно ? extends Object
List<?> list1;
List<? extends Object> list2; // То же самое
Multiple bounds
// В type parameter
<T extends Number & Comparable<T>>
// Нельзя в wildcard напрямую
List<? extends Number & Comparable> // ❌ Синтаксическая ошибка
// Но можно использовать type parameter
<T extends Number & Comparable<T>> void method(List<? extends T> list) {
// ...
}
Wildcards vs Type Parameters
Когда использовать Wildcard?
✅ Используй wildcard когда:
- Type используется один раз в сигнатуре
- Нет взаимосвязи между типами
- Нужна гибкость без введения type parameter
// ✅ Wildcard - тип используется один раз
void print(List<?> list) {
System.out.println(list);
}
// ✅ Wildcard - нет взаимосвязи между типами аргументов
void process(List<? extends Number> numbers, Set<String> names) {
// ...
}
Когда использовать Type Parameter?
✅ Используй type parameter когда:
- Type используется несколько раз
- Есть взаимосвязь между типами
- Нужен возвращаемый тип
- Нужны multiple bounds
// ✅ Type parameter - взаимосвязь между типами
<T> void copy(List<T> source, List<T> dest) {
// T связывает source и dest
}
// ✅ Type parameter - возвращаемый тип
<T> T getFirst(List<T> list) {
return list.get(0);
}
// ✅ Type parameter - multiple bounds
<T extends Number & Comparable<T>> T max(List<T> list) {
// ...
}
Сравнительная таблица
| Критерий | Wildcard | Type Parameter |
|---|---|---|
| Количество использований | 1 раз | Много раз |
| Возвращаемый тип | ❌ Нет | ✅ Да |
| Multiple bounds | ❌ Нет | ✅ Да |
| Взаимосвязь типов | ❌ Нет | ✅ Да |
| Простота | ✅ Проще | ❌ Сложнее |
| Гибкость | ✅ Больше | ❌ Меньше |
Best Practices
✅ DO (рекомендуется):
-
Используй PECS принцип
<T> void copy( List<? extends T> source, // Producer - extends List<? super T> dest // Consumer - super ) -
Используй
?когда тип не важенvoid printSize(Collection<?> c) { // ✅ System.out.println(c.size()); } -
Используй wildcards для гибкости API
interface Collection<E> { boolean addAll(Collection<? extends E> c); // ✅ Гибко } -
Используй type parameter когда есть взаимосвязь
<T> void swap(List<T> list, int i, int j) // ✅ -
Документируй wildcard bounds
/** * @param numbers список чисел для суммирования (producer) */ double sum(List<? extends Number> numbers)
❌ DON’T (не рекомендуется):
-
Не используй wildcard когда нужен type parameter
// ❌ Плохо List<?> reverse(List<?> list) // ✅ Хорошо <T> List<T> reverse(List<T> list) -
Не используй nested wildcards без необходимости
// ❌ Слишком сложно List<? extends List<? extends Number>> lists // ✅ Проще List<List<? extends Number>> lists -
Не путай extends и super
// ❌ Неправильно для чтения void print(List<? super String> list) // ✅ Правильно void print(List<? extends String> list) -
Не используй wildcards в return type без необходимости
// ❌ Неудобно для вызывающего кода List<?> getList() // ✅ Лучше <T> List<T> getList() -
Не создавай экземпляры с wildcard
// ❌ Нельзя List<?> list = new ArrayList<?>(); // ✅ Можно List<?> list = new ArrayList<String>();
Продвинутые примеры
1. Flexible Map Operations
public class MapUtils {
// Копирование с преобразованием типов
public static <K, V, KK extends K, VV extends V> void putAll(
Map<K, V> target,
Map<? extends KK, ? extends VV> source
) {
for (Map.Entry<? extends KK, ? extends VV> entry : source.entrySet()) {
target.put(entry.getKey(), entry.getValue());
}
}
}
// Использование
Map<Object, Object> target = new HashMap<>();
Map<String, Integer> source = Map.of("one", 1, "two", 2);
MapUtils.putAll(target, source); // ✅ String->Object, Integer->Object
2. Comparator с Wildcards
// Comparator для T или его супертипа
public static <T> void sort(
List<T> list,
Comparator<? super T> comparator
) {
// Можем использовать comparator для T или его родителей
list.sort(comparator);
}
// Использование
List<String> strings = Arrays.asList("banana", "apple", "cherry");
// Comparator<Object> может сравнивать String
Comparator<Object> objectComparator = Comparator.comparing(Object::toString);
sort(strings, objectComparator); // ✅ OK
3. Recursive Wildcards
// Enum pattern
public abstract class Enum<E extends Enum<E>> implements Comparable<E> {
@Override
public int compareTo(E other) {
return name().compareTo(other.name());
}
}
// Использование с wildcard
public static void printEnums(List<? extends Enum<?>> enums) {
for (Enum<?> e : enums) {
System.out.println(e.name());
}
}
Заключение
Ключевые моменты:
- Wildcards обеспечивают гибкость при работе с дженериками
?- неизвестный тип, используй когда тип не важен? extends T- Producer (читаем), верхняя граница? super T- Consumer (пишем), нижняя граница- PECS - Producer Extends, Consumer Super
Правила выбора:
Читаем из коллекции? → ? extends T
Пишем в коллекцию? → ? super T
И читаем И пишем? → T (без wildcard)
Тип вообще не важен? → ?
Когда использовать wildcards:
- ✅ API должен быть гибким
- ✅ Тип используется один раз
- ✅ Нет взаимосвязи между типами
- ✅ Следуй PECS принципу
Когда НЕ использовать wildcards:
- ❌ Возвращаемый тип метода
- ❌ Создание экземпляров
- ❌ Type используется многократно
- ❌ Нужны multiple bounds
Wildcards - мощный инструмент для создания гибкого и type-safe API в Java.