Ограниченные параметры типа в Java
Ограничение обобщённых типов Java с помощью bounded type parameters и extends — одиночные и множественные границы.
Ограниченный параметр типа — это параметр типа с конструкцией extends, ограничивающей допустимые типы-заменители. Без ограничения T рассматривается как Object — компилятор не знает, есть ли у T методы compareTo, intValue или какие-либо другие. С ограничением вы даёте компилятору обещание о том, чем является T, и в ответ компилятор позволяет вызывать методы, которые следуют из этого обещания. Практически каждый интересный обобщённый метод имеет хотя бы одно ограничение где-нибудь в сигнатуре.
Базовый синтаксис: <T extends Bound>
Синтаксис — ключевое слово extends между параметром и его ограничением — одно и то же ключевое слово вне зависимости от того, является ли ограничение классом или интерфейсом:
public static <T extends Number> double sum(List<T> values) {
double total = 0;
for (T n : values) total += n.doubleValue(); // legal — T is-a Number
return total;
}Читайте <T extends Number> как «любой T, который является Number или его подклассом». Внутри тела можно обращаться с любым значением T как с Number — вызывать doubleValue(), intValue(), любой метод из публичного API Number.
Используйте так же, как любой другой обобщённый метод:
sum(List.of(1, 2, 3)); // T = Integer — fine
sum(List.of(1.5, 2.5)); // T = Double — fine
sum(List.of(1L, 2L, 3L)); // T = Long — fine
sum(List.of("a", "b")); // ❌ String is not a NumberБез ограничения sum вообще не смог бы попытаться вызвать doubleValue() — T был бы стёрт до Object, у которого этого метода нет. Именно ограничение делает тело метода законным.
extends работает и для интерфейсов
В обобщённых типах Java ключевое слово extends перегружено — оно означает «является подтипом», будь то класс или интерфейс. Отдельного ключевого слова implements в списке параметров типа нет:
public static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T candidate : list) {
if (candidate.compareTo(best) > 0) best = candidate;
}
return best;
}
max(List.of(3, 1, 4, 1, 5, 9)); // T = Integer
max(List.of("Ada", "Grace", "Linus")); // T = StringComparable<T> — это интерфейс, но синтаксис по-прежнему использует extends. Читайте <T extends Comparable<T>> как «любой T, сравнимый сам с собой» — именно это ограничение позволяет вызывать candidate.compareTo(best) внутри цикла.
Небольшая тонкость здесь — <T> внутри ограничения: это саморекурсивная форма, которую мы встречали в разделе про обобщённые интерфейсы. Она обеспечивает, что compareTo принимает именно T, а не произвольный Comparable. Без неё можно было бы сравнивать Integer со String, и система типов этого не заметила бы.
Множественные ограничения
Параметр типа может иметь более одного ограничения; они объединяются с помощью &:
public static <T extends Number & Comparable<T>> T maxNumber(List<T> list) {
T best = list.get(0);
for (T n : list) {
if (n.compareTo(best) > 0) best = n;
}
return best;
}Теперь T должен быть одновременно Number и Comparable<T>. Внутри тела можно вызывать любые методы из обоих. Integer, Long, Double, BigDecimal удовлетворяют обоим условиям — можно передать любой из них.
Несколько правил:
- Ограничение-класс (если есть) должно стоять первым.
<T extends Number & Comparable<T>>допустимо;<T extends Comparable<T> & Number>— нет. - Не более одного ограничения-класса. В Java одиночное наследование классов, так что это вытекает из правил самого языка.
- Ограничений-интерфейсов может быть сколько угодно. Добавляйте столько, сколько действительно нужно; на практике два — обычный максимум, после которого стоит пересмотреть дизайн.
Правило «класс — первым» связано с байт-кодом: стирание заменяет T его самым левым ограничением, поэтому крайняя левая позиция особенная. Подробнее об этом — в разделе Type Erasure.
Ограничения на параметры типа уровня класса
Ограничения не уникальны для методов. Обобщённый класс или интерфейс может ограничивать свои параметры типа в объявлении:
public class SortedBag<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items);
}
public T smallest() { return items.get(0); }
}
SortedBag<Integer> bag = new SortedBag<>(); // OK — Integer is Comparable<Integer>
// SortedBag<Object> bag2 = new SortedBag<>(); // ❌ Object is not Comparable<Object>Это правильное место для ограничения, применимого ко всему классу. Каждый метод, каждое поле, каждая операция по умолчанию имеет доступ к этому ограничению.
Нижние ограничения относятся к wildcard-ам, а не к параметрам типа
Распространённая путаница: параметры типа могут иметь верхние ограничения (extends), но не нижние (super). Это допустимо:
public static <T extends Number> void foo(T x) { ... }А это — нет:
public static <T super Integer> void bar(T x) { ... } // ❌ no such syntaxНижние ограничения в обобщённых типах Java существуют — но только в wildcard-ах, токене ?, а не в именованных параметрах типа. Полную историю ? extends / ? super мы рассмотрим в разделе Wildcards; пока достаточно знать, что super работает с ?, но не с T.
Когда добавлять ограничение
По умолчанию ответ — «без ограничения»: чем слабее ограничение, тем больше типов принимает метод. Добавляйте ограничение тогда и только тогда, когда тело метода должно вызвать конкретный метод у T:
- «Мне нужно сравнивать значения
T» →<T extends Comparable<T>>. - «Мне нужно складывать значения
Tкак числа» →<T extends Number>. - «Мне нужно читать поля
Tкак уUser» →<T extends User>. - «Мне нужно, чтобы
Tреализовывал auto-closeable для использования в try-with-resources» →<T extends AutoCloseable>.
Если вы не вызываете метод у T, ограничение не нужно — добавление ограничения без причины лишь сужает ваш API.
Практический пример: ограниченный between и отсортированный top
Два метода, каждый с ограничением своего стиля. between использует Comparable<T> для сужения диапазона; topN использует Number & Comparable<T>, чтобы и сортировать, и считать числовую сумму.
between принимает любой Comparable — включая Character, который является Comparable<Character> и не имеет ничего общего с числами. topN уже, так как ему нужны и упорядочивание (для сортировки), и числовые значения (для суммы), поэтому он объединяет два ограничения через &. Каждый метод запрашивает ровно те возможности, которые использует, — не больше.
Что дальше
Ограничение на параметр типа фиксирует конкретный тип в месте вызова — List<Integer> означает «этот список, этот метод, этот тип». Иногда хочется быть менее конкретным: «список какого-то Number — может быть Integer, может быть Double, неважно». Для этого и существуют wildcards, которые устраняют один из самых распространённых источников путаницы в системе типов Java. Продолжайте читать: Java Generic Wildcards.