W3docs

Ограничения обобщённых типов Java

Что нельзя делать с обобщёнными типами Java: примитивы, статические параметры типа, generic-массивы и другие ограничения.

Эта глава — каталог вещей, которые нельзя делать с обобщёнными типами Java, с кратким объяснением причин каждого запрета. Почти все ограничения сводятся к одному факту, который вы узнали в предыдущей главе: параметр типа стирается до генерации байткода, поэтому всё, что требует его присутствия во время выполнения, работать не будет. Читайте эту главу как итоговую справочную карточку к разделу — это список ситуаций «я попробовал, компилятор пожаловался», собранных на одной странице.

1. Нельзя использовать примитивы в качестве аргументов типа

List<int> ints = new ArrayList<>();        // ❌
List<Integer> ints = new ArrayList<>();    // ✓

Стёртая форма List<E> на уровне байткода хранит элементы как Object, а примитивы не являются Object-ами. Решение — соответствующий класс-обёртка: Integer, Long, Double, Boolean, Character и т. д. Автоупаковка затем устраняет разрыв: ints.add(5) и int x = ints.get(0) оба работают, при условии, что каждый элемент платит за объект Integer в куче.

Project Valhalla — долгосрочная инициатива, цель которой сделать List<int> по-настоящему рабочим через value-типы и специализированные обобщения. По состоянию на Java 25 она ещё не завершена.

2. Нельзя писать new T()

public class Box<T> {
  public T newInstance() { return new T(); }      // ❌
}

Во время выполнения T не существует — JVM видит только Object (или границу). У неё нет объекта класса, чтобы вызвать конструктор, и нет способа узнать, какой именно конструктор нужен. Стандартный обходной путь — передать фабрику в качестве параметра:

public class Box<T> {
  public T newInstance(Supplier<T> factory) { return factory.get(); }
}

Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);

Supplier<T> несёт реальную фабрику во время выполнения — то, чего параметр типа никогда не мог.

3. Нельзя использовать T.class или instanceof T

public <T> boolean isIt(Object o) {
  return o instanceof T;        // ❌
}

public <T> Class<T> klass() {
  return T.class;               // ❌
}

Снова: T не существует во время выполнения. В обоих случаях решение — передать токен Class<T> в качестве аргумента:

public <T> boolean isIt(Object o, Class<T> type) {
  return type.isInstance(o);
}

Class.isInstance(Object) — это рефлективная форма instanceof, которая работает с объектом Class, переданным явно. Стандартная библиотека широко применяет этот подход: Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), JSON-десериализаторы и т. д.

4. Нельзя создавать массивы обобщённого типа

T[] arr = new T[10];              // ❌ — generic array creation
List<String>[] lists = new List<String>[10];   // ❌ — same

Здесь всё немного тоньше. Массивы в Java овеществленыInteger[] знает во время выполнения, что он является Integer[], и при записи выполняет проверку. Но обобщения стираются — JVM не может отличить List<String>[] от List<Integer>[]. Если бы оба ограничения не соблюдались, несколько строк кода могли бы привести к порче кучи:

List<String>[] strs = new List<String>[1];   // pretend this is legal
Object[] objs = strs;                         // arrays are covariant
objs[0] = List.of(42);                        // stores an Integer list
String s = strs[0].get(0);                    // KABOOM

Вместо того чтобы допустить это, компилятор запрещает создание generic-массивов.

Обходные пути:

  • Используйте (T[]) new Object[n] с @SuppressWarnings("unchecked") (это показано в примере с generic Stack ранее). Безопасно, если массив используется внутренне и никогда не «утекает» наружу как T[].
  • Или просто используйте List<T> вместо массива. В девяти случаях из десяти это правильный ответ.

5. Нельзя объявлять статические поля параметра типа

public class Box<T> {
  private static T defaultValue;       // ❌
  public  static T empty() { ... }     // ❌
}

Параметр типа принадлежит экземпляру — каждый Box<...> имеет собственный T. Статические члены принадлежат самому классу, у которого нет T. Эти две области видимости не связаны.

Если вам нужен статический метод, полиморфный по типу, объявите его собственный параметр типа (это рассматривалось в главе о generic methods):

public class Box<T> {
  public static <U> Box<U> empty() { return new Box<>(null); }
}

<U> является локальным для метода — независимым от любого T на уровне класса.

6. Нельзя создавать обобщённые типы исключений

public class MyException<T> extends Exception { ... }    // ❌

Таблицы обработки исключений JVM ищут блоки catch по стёртому классу. Если два разных обобщённых типа исключений стирались бы до одного класса, блок catch (MyException<String> e) перехватывал бы и MyException<Integer> — что неслышно нарушало бы систему типов. Чтобы не разбираться с этим, Java запрещает такое объявление напрямую. Также нельзя использовать параметр обобщённого типа в предложении catch:

try { ... } catch (T e) { ... }                          // ❌

Если исключению действительно нужно нести типизированную нагрузку, храните её в обобщённом поле необобщённого исключения:

public class TaggedException extends Exception {
  public final Object payload;
  public TaggedException(String message, Object payload) {
    super(message);
    this.payload = payload;
  }
}

Или сузьте место выброса исключения и оставьте типизированные данные для нормальных путей возврата.

7. Нельзя создавать перегрузки, различающиеся только обобщёнными параметрами

public void process(List<String> list)  { ... }
public void process(List<Integer> list) { ... }    // ❌ — both erase to process(List)

После стирания оба метода имеют сигнатуру process(List). Java разрешает перегрузки по стёртым сигнатурам, поэтому различить их невозможно. Решение — дать им разные имена: processStrings и processInts — или принять List<Object> и проверять тип во время выполнения.

8. Обобщённые типы инвариантны

Это не правило «компилятор отвергает это», а правило «компилятор отвергает то, что вы ожидали считать допустимым». Мы подробно рассмотрели его в главе о Wildcards:

List<Integer> ints = ...;
List<Number>  nums = ints;       // ❌ — generic types are invariant
List<? extends Number> nums = ints;   // ✓ — wildcard restores the flexibility

Это ограничение наиболее часто встречается на практике. Подстановочные символы служат предохранительным клапаном.

9. Нельзя создавать обобщённые перечисления

public enum Box<T> {                                   // ❌
  EMPTY, FULL;
  T value;
}

Перечисления транслируются в один класс с фиксированным набором констант — нет способа обеспечить константам единый осмысленный T. Обычное решение — сделать методы обобщёнными, а не само перечисление:

public enum Box {
  EMPTY, FULL;
  public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}

Или, если каждой константе действительно нужен собственный тип, используйте не-enum иерархию классов и Map<Name, Box>.

10. Вызов обобщённого метода через сырой тип

List rawList = new ArrayList();
rawList.add("hi");                  // unchecked-warning, but allowed
List<String> typed = rawList;       // unchecked-warning, dangerous

Смешивание сырых и обобщённых типов отключает все проверки обобщений на этапе компиляции для данной переменной. Компилятор выдаст предупреждение (Unchecked call to add(E) as a member of raw type java.util.List), а игнорирование предупреждения возвращает вам ловушку времён до Java 5 — значения неверного типа незаметно проникают внутрь и взрываются при следующем чтении.

Сырые типы существуют ради обратной совместимости, а не как функциональная возможность. В любом новом коде воспринимайте это предупреждение как ошибку.

Рабочий пример: каждое ограничение рядом с другим

Программа ниже пытается выполнить каждое из запрещённых действий (закомментировано, чтобы файл компилировался), а затем показывает канонический обходной путь для каждого. Читайте комментарии — они соответствуют пронумерованным ограничениям выше.

java— editable, runs on the server

Каждое пронумерованное ограничение соответствует однострочному обходному пути в программе. Общая закономерность одна: всё, что нуждается в параметре типа во время выполнения, получает эту информацию явно — токен Class<T>, Supplier<T>, параметр типа на уровне метода, подстановочный символ. Стирание убрало неявную форму; вы возвращаете её на границе API.

Часть 10 завершена

Обобщения — самая глубокая отдельная функция языка за пределами самой JVM. Теперь у вас есть рабочий словарь: параметры типа в классах, методах и интерфейсах; границы; подстановочные символы и PECS; стирание типов; и каталог ограничений, которые стирание накладывает на дизайн. Каждый современный Java API формируется этими правилами, и читать библиотечный код (или проектировать свой собственный) значительно легче, когда эта модель у вас в голове.

Что дальше

Обобщения не являются самоцелью — они существуют потому, что Java нуждалась в способе выразить «контейнер из T», не копируя класс для каждого типа элементов. Следующая часть книги — это то место, для которого весь механизм обобщений из этой части был негласно готовил вас: Collections Framework. List, Set, Map, Queue и десятки реализаций за ними — все параметризованные, все спроектированные по правилам, которые вы только что изучили. Продолжайте с введения в Java Collections.

Практика

Практика
Вы хотите написать обобщённый метод, возвращающий `true`, если аргумент является экземпляром `T`. Вы пишете `<T> boolean isIt(Object o) { return o instanceof T; }`. Компилятор отклоняет его. Какое стандартное решение?
Вы хотите написать обобщённый метод, возвращающий `true`, если аргумент является экземпляром `T`. Вы пишете `<T> boolean isIt(Object o) { return o instanceof T; }`. Компилятор отклоняет его. Какое стандартное решение?
Was this page helpful?