0Pricing
Java Academy · Урок

Подстановочные типы с верхней границей

? 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 — локальная установка не требуется.

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

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