Подстановочные типы с верхней границей
? extends T для производителей
«Подстановочные типы с верхней границей» — бесплатный урок Java Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Обобщения инвариантны
В Java обобщения являются инвариантными. List<Integer> — это не List<Number>, хотя целочисленный тип относится к числовым типам.
Новичков это удивляет, но благодаря этому система типов остаётся безопасной. Подстановочные типы безопасно ослабляют это ограничение.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
// List<Number> nums = ints; // would NOT compile
System.out.println("List<Integer> is not List<Number>");
System.out.println(ints);
}
}Подстановочный тип с верхней границей
? extends T — это подстановочный тип с верхней границей. Он означает «некоторый неизвестный тип, совпадающий с указанным типом или являющийся его подтипом».
List<? extends Number> может обозначать List<Integer>, List<Double> или List<Number>.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<? extends Number> a = List.of(1, 2, 3);
List<? extends Number> b = List.of(1.5, 2.5);
System.out.println(a.get(0));
System.out.println(b.get(0));
}
}Чтение из источника данных
С помощью ? extends Number можно безопасно читать элементы как Number.
Каков бы ни был фактический тип элемента, гарантируется, что он относится как минимум к числовым типам, поэтому чтение безопасно с точки зрения типов.
import java.util.List;
public class Main {
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
}
}В него нельзя записывать
Но есть важный нюанс: в список ? extends Number нельзя добавлять элементы (кроме null).
Компилятор не знает точный тип элементов. Это может быть List<Integer>, поэтому добавление Double было бы небезопасным.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<? extends Number> list = new ArrayList<Integer>();
// list.add(1); // does NOT compile
// list.add(1.5); // does NOT compile
System.out.println("Cannot add to ? extends Number");
System.out.println("size = " + list.size());
}
}Почему запись запрещена
Представьте, что это было бы разрешено. Можно было бы передать List<Integer> как List<? extends Number>, а затем добавить Double.
Позднее код, читающий список как List<Integer>, получил бы ClassCastException. Компилятор предотвращает это, запрещая запись.
import java.util.List;
public class Main {
public static void main(String[] args) {
// Conceptual: this is why add is blocked
System.out.println("Writing a Double into a List<Integer> would corrupt it");
System.out.println("so ? extends bans all adds");
}
}Сценарий использования производителя
Используйте ? extends T, когда метод только производит значения, то есть читает их для Вас.
Метод, копирующий элементы из исходной коллекции, — прекрасный пример.
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<Number> copyOut(List<? extends Number> source) {
List<Number> dest = new ArrayList<>();
for (Number n : source) dest.add(n);
return dest;
}
public static void main(String[] args) {
System.out.println(copyOut(List.of(1, 2, 3)));
}
}Параметры типов с ограничениями и подстановочные знаки
Не путайте <T extends Number> (ограниченный параметр типа) с ? extends Number (подстановочным знаком).
- Параметр типа: задаёт имя типа, которое используется во всём методе.
- Подстановочный знак: анонимный неизвестный подтип, которому не нужно задавать имя.
import java.util.List;
public class Main {
static <T extends Number> T first(List<T> list) { return list.get(0); }
public static void main(String[] args) {
Integer i = first(List.of(10, 20));
System.out.println(i);
}
}Несколько ограничений
Параметр типа может иметь несколько ограничений, объединённых с помощью &, например <T extends Number & Comparable<T>>.
Подстановочные знаки поддерживают только одно ограничение сверху, поэтому для нескольких ограничений нужен именованный параметр типа.
import java.util.List;
public class Main {
static <T extends Number & Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 9, 1, 7)));
}
}Подстановочные знаки с Collections.max
JDK широко использует ограничения сверху, например при суммировании или просмотре коллекции только для чтения.
Здесь мы находим наибольший элемент списка, типизированного как источник.
import java.util.List;
public class Main {
static double largest(List<? extends Number> nums) {
double max = Double.NEGATIVE_INFINITY;
for (Number n : nums) max = Math.max(max, n.doubleValue());
return max;
}
public static void main(String[] args) {
System.out.println(largest(List.of(4, 2, 9, 1)));
System.out.println(largest(List.of(0.5, 9.9, 3.3)));
}
}null — единственное записываемое значение
Единственное, что можно добавить в коллекцию ? extends T, — это null, поскольку null можно присвоить любому ссылочному типу.
Это редко бывает полезно, но объясняет, почему add запрещён не полностью, а лишь практически недоступен.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<? extends Number> list = new ArrayList<Number>();
// Only null is assignable; demonstrated conceptually
System.out.println("Only null could be added; we avoid that");
System.out.println("size = " + list.size());
}
}Чтение как Object всегда возможно
Поскольку каждый тип является подтипом Object, элементы любого списка с подстановочным знаком всегда можно прочитать как Object.
При ограничении сверху Number Вы дополнительно получаете возможность сразу читать их как Number.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<? extends Number> nums = List.of(1, 2.0, 3L);
for (Object o : nums) System.out.println(o.getClass().getSimpleName());
}
}Быстрая проверка
Проверьте, насколько хорошо Вы поняли ограничения сверху.
Итоги
Вы изучили подстановочные знаки с ограничением сверху:
? extends Tозначает неизвестный подтип T.- Элементы можно читать как T (это источник).
- Нельзя добавлять ненулевые элементы.
- Обобщения инвариантны, а подстановочные знаки безопасно ослабляют это ограничение.
Далее — зеркальная противоположность: подстановочные знаки с ограничением снизу.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<? extends Number> nums = List.of(1, 2, 3);
System.out.println("Upper bound recap: read " + nums.get(0));
}
}Часто задаваемые вопросы
Урок «Подстановочные типы с верхней границей» бесплатный?
Да — полный текст урока «Подстановочные типы с верхней границей» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Подстановочные типы с верхней границей»?
? extends T для производителей Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Подстановочные типы с верхней границей»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Подстановочные типы с верхней границей
- Подстановочные типы с нижней границей
- Принцип PECS
- Подстановочные типы в API