Дженерики позволяют нам работать однообразно с любым типом данных, но иногда возникает задача создать дженерик для определенного набора типов.
Ограничение сверху (extends)
Возьмем для примера задачу поиска среднего значения для списка чисел. Для Integer
реализация метода выглядит так:
public class Application {
public static Double average(List<Integer> numbers) {
// Double
var sum = 0.0;
for (var number : numbers) {
sum += number;
}
return sum / numbers.size();
}
public static void main(String[] args) {
var numbers = List.of(1, 2, 3, 4, 5);
System.out.println(average(numbers)); // => 3.0
}
}
Если сделать из метода average()
дженерик, то он не сработает, так как тип T
будет Object
, для которого операция сложения не определена.
public static <T> Double average(List<T> numbers) {
var sum = 0.0;
for (var number : numbers) {
// The operator += is undefined for the argument type(s) double, T
sum += number;
}
return sum / numbers.size();
}
Для подобных задач в Java есть механизм Wildcard, с его помощью можно уточнить типы, с которыми работает дженерик. В нашем случае и Integer
и Double
являются подтипами Number
, а значит мы можем написать так: List<? extends Number>
. В таком случае в метод попадут только числа, какими бы они не были, а типом параметра numbers
станет List<Number>
.
public static <T> Double average(List<? extends Number> numbers) {
var sum = 0.0;
for (var number : numbers) {
sum += number;
}
return sum / numbers.size();
}
Обновленный код почти работает. Он все еще выдает ошибку The operator += is undefined for the argument type(s) double, Number, так как во время сложения получается что тип переменной sum
это Double
, а тип переменной number
- Number
. Эта задача решается за счет метода doubleValue()
определенного у Number
, который любое число преобразует в Double
. Рабочий код:
public static <T> Double average(List<? extends Number> numbers) {
var sum = 0.0;
for (var number : numbers) {
sum += number.doubleValue();
}
return sum / numbers.size();
}
Этот подход называется ограничением сверху, потому что с помощью <? extends Number>
мы указываем, что тип, который может быть передан в метод, должен быть подтипом Number
. Другими словами, мы задаем верхнюю границу для возможных типов, которые могут быть использованы. Это позволяет работать с любыми типами, которые являются наследниками Number
(например, Integer
, Double
, Float
и т.д.). Но при этом запрещает передачу типов, которые не входят в эту иерархию.
Ограничение снизу (super)
Мы разобрали, как Wildcard с ограничением сверху позволяет работать с типами, которые являются подтипами определенного класса или интерфейса. Это особенно полезно, когда нам нужно читать данные из коллекции, сохраняя гибкость в выборе типов. Но что, если нам нужно не только читать, но и записывать данные в коллекцию? Здесь на помощь приходит противоположный подход — ограничение снизу.
Ограничение снизу применяется, когда мы хотим добавлять элементы в коллекцию, но не хотим ограничиваться конкретным типом. Например, если у нас есть метод, который добавляет числа в список, мы можем использовать super
, чтобы этот метод мог работать не только с List<Integer>
, но и с List<Number>
или даже List<Object>
.
Рассмотрим пример:
Copy
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
Метод addNumbers()
принимает список, который может содержать элементы типа Integer
или его супертипы (например, Number
или Object
). Таким образом, мы ограничиваем тип снизу. Это позволяет добавлять целые числа в список, даже если он объявлен как List<Number>
или List<Object>
var numbers = new ArrayList<Number>();
addNumbers(numbers); // Добавляем целые числа в список Number
System.out.println(numbers); // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Здесь мы передаем список List<Number>
в метод addNumbers()
, и он успешно добавляет в него целые числа
Принцип PECS
Чтобы лучше понять, когда использовать extends
а когда super
, полезно запомнить принцип PECS (Producer Extends, Consumer Super). Этот принцип помогает определить, какой тип Wildcard следует использовать в зависимости от роли коллекции.
Если коллекция является источником данных (производителем, producer), используйте extends
. Например, List<? extends Number>
подходит для чтения чисел из списка.
Если вы записываете данные в коллекцию, то коллекция является приемником данных (потребителем, consumer). В этом случае используйте super
. Например, List<? super Integer>
подходит для добавления целых чисел в список
Wildcard с ограничениями не часто используется в прикладном коде, но часто используется в библиотеках. Поэтому его нужно знать на начальном этапе, хотя бы для того, чтобы читать документацию и понимать что там написано.
Например, вот как выглядит определение метода Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// Реализация копирования
}
Здесь dest
— это список, который может содержать элементы типа T
или его супертипы (потребитель), а src
— это список, который может содержать элементы типа T
или его подтипы (производитель).
Выводы
- Wildcard с ограничениями
extends
иsuper
делает Generics в Java более гибкими и безопасными. - Используя
extends
, мы можем работать с коллекциями, которые производят данные - C помощью
super
мы можем работать с коллекциями, которые потребляют данные - Принцип PECS помогает запомнить, когда и какой тип ограничения использовать
Для более глубокого понимания темы мы рекомендуем просмотреть видео лекцию, которая является дополнительным материалом к данному курсу:
Дополнительные материалы

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.