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.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✅ TConsumer - пишем элементы типа 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
}

Как работает:

  1. List<?> передается в swapHelper
  2. Компилятор выводит T = capture of ?
  3. В swapHelper тип известен как T
  4. Теперь можно читать и писать

Пример: 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) {
    // ...
}

Сравнительная таблица

КритерийWildcardType Parameter
Количество использований1 разМного раз
Возвращаемый тип❌ Нет✅ Да
Multiple bounds❌ Нет✅ Да
Взаимосвязь типов❌ Нет✅ Да
Простота✅ Проще❌ Сложнее
Гибкость✅ Больше❌ Меньше

Best Practices

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

  1. Используй PECS принцип

    <T> void copy(
        List<? extends T> source,   // Producer - extends
        List<? super T> dest        // Consumer - super
    )
    
  2. Используй ? когда тип не важен

    void printSize(Collection<?> c) {  // ✅
        System.out.println(c.size());
    }
    
  3. Используй wildcards для гибкости API

    interface Collection<E> {
        boolean addAll(Collection<? extends E> c);  // ✅ Гибко
    }
    
  4. Используй type parameter когда есть взаимосвязь

    <T> void swap(List<T> list, int i, int j)  // ✅
    
  5. Документируй wildcard bounds

    /**
     * @param numbers список чисел для суммирования (producer)
     */
    double sum(List<? extends Number> numbers)
    

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

  1. Не используй wildcard когда нужен type parameter

    // ❌ Плохо
    List<?> reverse(List<?> list)
    
    // ✅ Хорошо
    <T> List<T> reverse(List<T> list)
    
  2. Не используй nested wildcards без необходимости

    // ❌ Слишком сложно
    List<? extends List<? extends Number>> lists
    
    // ✅ Проще
    List<List<? extends Number>> lists
    
  3. Не путай extends и super

    // ❌ Неправильно для чтения
    void print(List<? super String> list)
    
    // ✅ Правильно
    void print(List<? extends String> list)
    
  4. Не используй wildcards в return type без необходимости

    // ❌ Неудобно для вызывающего кода
    List<?> getList()
    
    // ✅ Лучше
    <T> List<T> getList()
    
  5. Не создавай экземпляры с 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());
    }
}

Заключение

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

  1. Wildcards обеспечивают гибкость при работе с дженериками
  2. ? - неизвестный тип, используй когда тип не важен
  3. ? extends T - Producer (читаем), верхняя граница
  4. ? super T - Consumer (пишем), нижняя граница
  5. PECS - Producer Extends, Consumer Super

Правила выбора:

Читаем из коллекции?       → ? extends T
Пишем в коллекцию?         → ? super T
И читаем И пишем?          → T (без wildcard)
Тип вообще не важен?       → ?

Когда использовать wildcards:

  • ✅ API должен быть гибким
  • ✅ Тип используется один раз
  • ✅ Нет взаимосвязи между типами
  • ✅ Следуй PECS принципу

Когда НЕ использовать wildcards:

  • ❌ Возвращаемый тип метода
  • ❌ Создание экземпляров
  • ❌ Type используется многократно
  • ❌ Нужны multiple bounds

Wildcards - мощный инструмент для создания гибкого и type-safe API в Java.