0Pricing
Java Academy · Урок

Подстановочные типы в 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 — локальная установка не требуется.

Все уроки этого курса

  1. Подстановочные типы с верхней границей
  2. Подстановочные типы с нижней границей
  3. Принцип PECS
  4. Подстановочные типы в API
← Назад к Java Academy