Подстановочные типы в API
Проектируйте гибкие обобщённые методы
«Подстановочные типы в API» — бесплатный урок Java Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Проектирование гибких методов
Подстановочные знаки — прежде всего инструмент для проектировщиков API. Удачно выбранный подстановочный знак позволяет вызывающему коду передавать больше типов без приведений.
Цель — максимальная гибкость для вызывающего кода и полная безопасность типов внутри метода.
import java.util.List;
public class Main {
static double total(List<? extends Number> items) {
double sum = 0;
for (Number n : items) sum += n.doubleValue();
return sum;
}
public static void main(String[] args) {
System.out.println(total(List.of(1, 2, 3)));
System.out.println(total(List.of(1.5, 2.5)));
}
}Неограниченные подстановочные знаки
List<?> — это неограниченный подстановочный знак: список некоторого неизвестного типа.
Используйте его, когда метод не зависит от типа элементов, например при подсчёте элементов или проверке пустоты.
import java.util.List;
public class Main {
static int size(List<?> any) {
return any.size();
}
public static void main(String[] args) {
System.out.println(size(List.of("a", "b")));
System.out.println(size(List.of(1, 2, 3)));
}
}List<?> и List<Object>
Это разные типы. List<Object> принимает только список, объявленный как Object. List<?> принимает список любого типа.
Параметру типа List<?> можно передать List<String>, но параметру типа List<Object> — нельзя.
import java.util.List;
public class Main {
static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
public static void main(String[] args) {
List<String> names = List.of("Ada", "Linus");
printAll(names); // works; List<Object> would reject this
}
}Когда параметр типа понятнее
Если Вам нужно обратиться к типу элемента больше одного раза, именованный параметр типа читается лучше, чем подстановочный знак.
Сравните void swap(List<?> l, int i, int j) (ему нужен закрытый вспомогательный метод) с аккуратным обобщённым вариантом.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
swap(list, 0, 2);
System.out.println(list);
}
}Приём с захватом во вспомогательном методе
Если открытый API должен использовать List<?>, можно делегировать работу закрытому обобщённому вспомогательному методу, чтобы захватить подстановочный знак.
Вспомогательный метод задаёт типу имя T и благодаря этому может записывать значения обратно в список.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void reverse(List<?> list) { reverseHelper(list); }
private static <T> void reverseHelper(List<T> list) {
for (int i = 0, j = list.size() - 1; i < j; i++, j--) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
reverse(list);
System.out.println(list);
}
}Параметры-источники в реальном API
Проектирование метода addAll, который получает данные из любого совместимого источника, предполагает ограничение сверху для источника.
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Main {
static <T> void addAll(List<T> target, Collection<? extends T> source) {
for (T t : source) target.add(t);
}
public static void main(String[] args) {
List<Number> nums = new ArrayList<>();
addAll(nums, List.of(1, 2, 3));
System.out.println(nums);
}
}Параметры-потребители в реальном API
API приёмника, записывающего результаты, использует ограничение снизу для целевого объекта, чтобы вызывающий код мог передавать контейнеры более общего типа.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void repeat(T value, int times, List<? super T> sink) {
for (int i = 0; i < times; i++) sink.add(value);
}
public static void main(String[] args) {
List<Object> out = new ArrayList<>();
repeat("hi", 3, out);
System.out.println(out);
}
}Подстановочные знаки во вложенных обобщениях
Во вложенных обобщениях часто нужны подстановочные знаки. Метод, принимающий список любого вида списков, использует List<? extends List<?>>.
import java.util.List;
public class Main {
static int countAll(List<? extends List<?>> lists) {
int total = 0;
for (List<?> inner : lists) total += inner.size();
return total;
}
public static void main(String[] args) {
System.out.println(countAll(List.of(List.of(1, 2), List.of("a", "b", "c"))));
}
}Избегайте избыточной параметризации
Подстановочные знаки нужны не каждому методу. Если вызывающий код всегда передаёт только один тип, обычный параметр понятнее.
Добавляйте подстановочные знаки, когда реальные вызывающие стороны сталкиваются с ограничениями инвариантности, а не заранее.
import java.util.List;
public class Main {
// Simple and clear; no wildcard needed for this internal use
static String joinStrings(List<String> parts) {
return String.join(", ", parts);
}
public static void main(String[] args) {
System.out.println(joinStrings(List.of("a", "b", "c")));
}
}Документирование назначения подстановочного знака
Подстановочный знак выражает намерение. ? extends T сообщает читателям: «Я только читаю это». ? super T означает: «Я только записываю это».
Выбор подходящего подстановочного знака — это форма самодокументируемого кода, которая одновременно обеспечивает соблюдение контракта.
import java.util.ArrayList;
import java.util.List;
public class Main {
// src: read-only producer; dest: write-only consumer
static <T> void transfer(List<? extends T> src, List<? super T> dest) {
for (T t : src) dest.add(t);
}
public static void main(String[] args) {
List<Number> dest = new ArrayList<>();
transfer(List.of(1, 2), dest);
System.out.println(dest);
}
}Обобщённый API стека
Знаменитый пример стека Блоха: pushAll получает значения от источника (ограничение сверху), а popAll производит значения в потребитель (ограничение снизу).
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Stack<E> {
private final Deque<E> items = new ArrayDeque<>();
void pushAll(Collection<? extends E> src) { for (E e : src) items.push(e); }
void popAll(Collection<? super E> dst) { while (!items.isEmpty()) dst.add(items.pop()); }
}
public static void main(String[] args) {
Stack<Integer> s = new Stack<>();
s.pushAll(List.of(1, 2, 3));
List<Number> out = new ArrayList<>();
s.popAll(out);
System.out.println(out);
}
}Быстрая проверка
Проверьте своё чутьё при проектировании API.
Итоги
Вы научились проектировать API с подстановочными знаками:
List<?>— для методов только для чтения, не зависящих от типа.- Применяйте PECS к параметрам для повышения гибкости.
- Используйте приём с закрытым вспомогательным методом и захватом, когда необходимо записывать через подстановочный знак.
- Избегайте подстановочных знаков в возвращаемых типах и не создавайте лишнюю параметризацию.
Вы завершили курс по подстановочным знакам и PECS.
public class Main {
public static void main(String[] args) {
System.out.println("Wildcards in APIs course complete");
}
}Часто задаваемые вопросы
Урок «Подстановочные типы в API» бесплатный?
Да — полный текст урока «Подстановочные типы в API» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Подстановочные типы в API»?
Проектируйте гибкие обобщённые методы Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Подстановочные типы в API»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Подстановочные типы с верхней границей
- Подстановочные типы с нижней границей
- Принцип PECS
- Подстановочные типы в API