W3docs

Подстановочные знаки в Java Generics

Ограниченные и неограниченные wildcards в Java generics, правило PECS и практические примеры использования.

Wildcard — это токен ?, который появляется в обобщённых типах вместо конкретного аргумента типа: List<?>, List<? extends Number>, List<? super Integer>. Это решение проблемы, с которой сталкиваешься почти сразу при написании обобщённого кода: List<Integer> не является подтипом List<Number>, хотя Integer является подтипом Number. Wildcards позволяют описать «список некоторых Number», не привязываясь к конкретному типу элементов — и они, пожалуй, самая запутанная часть системы типов Java.

Неочевидная отправная точка

Вот факт, который делает wildcards необходимыми:

List<Integer> ints  = List.of(1, 2, 3);
List<Number>  nums  = ints;            // ❌ does not compile

Несмотря на то что Integer extends Number, List<Integer> не расширяет List<Number>. Обобщённые типы инвариантныList<Sub> и List<Super> не связаны между собой независимо от того, какими являются Sub и Super.

Причина обоснована, пусть и удивительна. Если бы List<Integer> являлся List<Number>, можно было бы написать:

List<Number> nums = ints;     // pretend this is legal
nums.add(3.14);               // legal — 3.14 is a Number
int x = ints.get(3);          // KABOOM at runtime — it's a Double

Приведение типа в последней строке взорвалось бы. Чтобы этого не случилось, компилятор отвергает самый первый шаг: List<Integer> — не List<Number>. И точка.

Wildcards позволяют вернуть гибкость безопасным способом.

Неограниченный wildcard: List<?>

Самый простой wildcard — одиночный ? — «список некоторого неизвестного типа»:

public static void printAll(List<?> list) {
  for (Object o : list) System.out.println(o);
}

printAll(List.of(1, 2, 3));          // List<Integer> — OK
printAll(List.of("a", "b"));         // List<String>  — OK
printAll(new ArrayList<>());         // List<Object>  — OK

Внутри тела единственное, что можно делать с элементами List<?> — читать их как Object, потому что компилятор понятия не имеет, что такое ?. Добавить что-либо в List<?> нельзя (единственное исключение — null):

public static void corrupt(List<?> list) {
  list.add("hello");   // ❌ does not compile — ? is unknown
  list.add(null);      // ✓ — null is a value of every reference type
}

List<?> используется, когда нужно выразить: «принимаю любой список и буду только читать из него как из Object».

Ограниченный сверху wildcard: ? extends T

Когда нужно читать элементы как конкретный тип — например, обрабатывать их все как Number — используется ограниченный сверху wildcard:

public static double sum(List<? extends Number> list) {
  double total = 0;
  for (Number n : list) total += n.doubleValue();   // legal — every element IS-A Number
  return total;
}

sum(List.of(1, 2, 3));          // List<Integer> — OK, Integer extends Number
sum(List.of(1.5, 2.5));         // List<Double>  — OK
sum(List.of(1L, 2L, 3L));       // List<Long>    — OK

List<? extends Number> читается как «список некоторого конкретного типа, который является Number или подтипом Number». Читать из него как из Number можно. Добавлять в него ничего нельзя — снова с исключением для null — потому что компилятор не знает, какой именно подтип Number хранит список. Добавить Integer в List<? extends Number>, который на самом деле является List<Double>, означало бы испортить его; вместо того чтобы выяснять, какой это подтип, компилятор просто отклоняет любой add.

Ограниченный снизу wildcard: ? super T

Зеркальный вариант. ? super T означает «тип элементов списка — это T или некоторый супертип T»:

public static void addOneTwoThree(List<? super Integer> list) {
  list.add(1);
  list.add(2);
  list.add(3);
}

List<Integer> ints   = new ArrayList<>();   addOneTwoThree(ints);   // ✓
List<Number>  nums   = new ArrayList<>();   addOneTwoThree(nums);   // ✓ — Number is a supertype of Integer
List<Object>  objs   = new ArrayList<>();   addOneTwoThree(objs);   // ✓ — Object is too

Здесь можно безопасно добавлять любой Integer (или его подтип) — тип элементов списка гарантированно является Integer или каким-то его предком, так что Integer туда подходит. Что нельзя — так это вычитать элемент конкретного типа: всё, что можно сказать об элементе, — это то, что он Object, потому что реальный список может оказаться List<Object>.

Правило PECS

Существует один мнемонический приём, который рано или поздно запоминает каждый Java-разработчик:

PECS — Producer Extends, Consumer Super.

Это эмпирическое правило выбора wildcard:

  • Если параметр производит значения (вы читаете из него): используйте ? extends T.
  • Если параметр потребляет значения (вы пишете в него): используйте ? super T.

Каноническая сигнатура, которую оно порождает, — Collections.copy:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  for (int i = 0; i < src.size(); i++) {
    dest.set(i, src.get(i));
  }
}

src читается (он производит T) — ? extends T. В dest записывается (он потребляет T) — ? super T. Именно в этом причина асимметрии: одна и та же сигнатура работает независимо от того, является ли src List<Integer>, а destList<Number>, или наоборот, лишь бы T был точкой пересечения между ними.

Если вы ничего больше не запомните из этой главы, запомните PECS.

Когда не нужен wildcard

Если параметр и читается, и записывается в одном и том же методе, ни ? extends T, ни ? super T не подойдут — ни один не даст делать и то, и другое. В этом случае просто используйте обычный параметр типа:

public static <T> void swap(List<T> list, int i, int j) {
  T tmp = list.get(i);              // read
  list.set(i, list.get(j));         // write
  list.set(j, tmp);                 // write
}

Wildcard — правильный инструмент, когда одна сторона отношений «только читает» или «только пишет». Параметр типа — правильный инструмент, когда нужно говорить о конкретном типе элементов с обеих сторон.

Wildcards vs. ограниченные параметры типа

Сравним:

public static <T extends Number> double sumNamed(List<T> list)         { ... }
public static            double sumWildcard(List<? extends Number> list) { ... }

Функционально они принимают одно и то же множество аргументов. Различие — в том, что может сказать тело:

  • Именованная форма (<T extends Number>) даёт имя T — полезно, если нужно вернуть T, принять ещё один List<T> в качестве второго параметра или написать T tmp = list.get(0), чтобы сохранить точный тип элемента.
  • Wildcard-форма (? extends Number) не даёт имени — ссылаться на элементы можно только как на Number. Она компактнее в API (имя не просачивается в сигнатуру), но менее выразительна в теле.

Эмпирическое правило: если элементы нужны только как Number, wildcard — более лаконичный и чистый выбор. Если телу нужно говорить о конкретном T, дайте ему имя.

Развёрнутый пример: PECS на практике

Программа копирует элементы из одного списка в другой и вычисляет накопленную сумму — обе операции параметризованы по правилу PECS. Обратите внимание на вызовы: copyOf(intList, numberList) смешивает типы элементов, потому что wildcards позволяют списку с Number принимать значения Integer.

java— editable, runs on the server

sum принимает как List<Integer>, так и List<Double>, потому что wildcard говорит «некоторый подтип Number». fillWithSquares добавляет значения Integer в List<Number>, потому что wildcard говорит «любой список, который может хранить Integer или одного из его предков». copyTo использует оба варианта — источник является производителем, назначение — потребителем, а T — общий тип элемента, который компилятор выводит из согласования двух сторон.

Что дальше

Вы познакомились с четырьмя способами, которыми generics проявляются в исходном коде: классы, методы, интерфейсы и wildcards. Теперь заглянем на уровень ниже и посмотрим, как JVM реализует всё это на самом деле. Ответ — стирание типов — объясняет некоторые удивительные ограничения (нет new T(), нет instanceof T, нет обобщённых массивов) и является единственным озарением, которое делает историю generics в Java понятной. Продолжите с Java Type Erasure.

Практика

Практика
Вы пишете `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Почему именно такое сочетание wildcards?
Вы пишете `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Почему именно такое сочетание wildcards?
Was this page helpful?