W3docs

Введение в обобщения Java

Зачем нужны обобщения Java — безопасность типов, повторное использование кода и устранение приведений в коллекциях.

Generics — это возможность, которая позволяет классу, интерфейсу или методу работать с неопределённым типом, а затем компилятор фиксирует этот тип в том месте, где вы его используете. List<String> — это список строк, компилятор это знает, и любая попытка добавить в него Date будет отклонена до запуска программы. До появления обобщений в Java 5 тот же список был List из Object, и каждое чтение из него требовало написанного вручную приведения типа, которое могло сработать или нет во время выполнения. Обобщения превратили эту лотерею времени выполнения в проверку времени компиляции, и почти каждый современный API Java построен с их использованием.

Проблема, которую решают обобщения

Чтобы понять, зачем нужны обобщения, представьте Java без них. Контейнер, который хранит произвольные объекты, должен объявлять своё содержимое как Object:

// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");

String first = (String) names.get(0);   // cast required, never checked by the compiler

Два проблемы. Первая: приведение типа — это шум — каждое чтение из контейнера его требует. Вторая, и более серьёзная: ничто не мешает кому-то добавить в тот же список Date:

names.add(new java.util.Date());        // compiler is fine with this
String oops = (String) names.get(2);    // ClassCastException at runtime

Ошибка проявляется при чтении, далеко от места записи. Приведение лжёт — оно утверждает «это является строкой», и JVM обнаруживает обман только тогда, когда уже поздно дать вам полезный стек вызовов рядом с реальной проблемой.

Обобщения решают обе проблемы:

List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date());        // ❌ compile error — won't even build

String first = names.get(0);  // no cast — the compiler already knows it's a String

Угловая скобка <String> — это параметр типа. Она сообщает компилятору «этот список содержит строки», и с этого момента каждый add и get проверяется относительно этого обещания.

Три бесплатных преимущества

Обобщения дают три конкретных преимущества, и именно поэтому каждая коллекция, поток и optional в современном JDK является обобщённым:

  • Более строгие проверки во время компиляции. Вставка элемента неправильного типа, показанная выше, обнаруживается на этапе сборки, а не в продакшене. Целый класс ошибок ClassCastException просто перестаёт возникать.
  • Больше никаких приведений. Чтение из Map<String, User> возвращает User, а не Object, который нужно приводить. Меньше синтаксического шума, меньше читать, меньше поддерживать.
  • Повторное использование кода без копирования. Один класс List<E> работает для любого типа элементов. До обобщений стандартная библиотека либо принимала Object везде, либо поставляла StringList, IntList, DateList и так далее. Теперь вы пишете один класс и позволяете вызывающей стороне его параметризовать.

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

Первый обобщённый класс

Соглашение состоит в том, чтобы называть параметр типа одной заглавной буквой: T для обобщённого «типа», E для «элемента» коллекции, K/V для «ключа» и «значения» карты, R для «возвращаемого значения». Вот простейший возможный обобщённый класс — пара из двух объектов одного типа:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) {
    this.first  = first;
    this.second = second;
  }

  public T first()  { return first; }
  public T second() { return second; }
}

<T> после имени класса вводит параметр типа. Далее T можно использовать внутри класса везде, где мог бы стоять обычный тип. Вызывающая сторона выбирает T при создании объекта:

Pair<String>  names   = new Pair<>("Ada", "Grace");
Pair<Integer> scores  = new Pair<>(100, 87);
String n1 = names.first();      // already a String, no cast
int s1    = scores.first();     // auto-unboxed from Integer

Пустые <> справа (оператор ромба, Java 7+) говорят компилятору вывести тип из объявления слева — почти никогда не нужно повторять аргумент типа.

Что параметризуется, а что нет

Параметр типа может заменять:

  • Тип поля (private T value;)
  • Параметр или возвращаемый тип метода (public T get() { ... }, void put(T value))
  • Тип элементов массива этого типа (T[] items — с некоторыми оговорками)

Параметр типа не может заменять:

  • Примитив (Pair<int> недопустимо — используйте Pair<Integer> и позвольте автоупаковке сделать работу)
  • Тип статического поля или параметр типа статического метода (параметр принадлежит экземпляру, а не самому классу)
  • Целевой объект new T() или instanceof T — Java стирает обобщения во время выполнения, поэтому у программы нет T, который можно создать или проверить

Полный список «недопустимых действий» вынесен в отдельную главу в конце этой части — Java Generics Restrictions — после того как мы рассмотрим достаточно механизмов, чтобы правила обрели смысл.

Пример: безопасность типов против сырых типов, бок о бок

Программа ниже создаёт один и тот же контейнер дважды — один раз как сырой List (форма до обобщений) и один раз как List<String>. Оба компилируются; безопасным является только параметризованный.

java— editable, runs on the server

Сырая версия падает в середине итерации, потому что цикл доверял приведению, которому не следовало. Обобщённая версия сделала ту же ошибку невыразимой — неудачный add(42) в принципе не скомпилируется. Этот переход от времени выполнения к времени компиляции и есть главная причина существования обобщений.

Что рассматривается в этой части книги

Остальные главы этой части разбирают обобщения по частям:

  • Обобщённые классы — параметр типа на уровне класса, который вы только что видели, более подробно.
  • Обобщённые методы — методы, которые вводят собственный параметр типа, независимый от класса.
  • Обобщённые интерфейсы — проектирование контрактов API, параметризованных по типу.
  • Ограниченные параметры типа — указание «T должен расширять Number», чтобы можно было вызывать методы на T.
  • Wildcards? extends T, ? super T и правило PECS, определяющее, когда использовать каждый.
  • Стирание типов — как JVM реализует обобщения под капотом и почему некоторые ожидаемые вещи не работают.
  • Ограничения — каталог действий, которые язык запрещает, с объяснением причин.

Читайте по порядку — каждая глава предполагает знакомство с предыдущими.

Что дальше

Начните с самой распространённой формы — класс, поля и методы которого параметризованы по типу, выбранному вызывающей стороной. Перейдите к Java Generic Classes.

Практика

Практика
Метод объявляет `public static List getNames() { ... }` (без параметра типа у списка). Вызывающая сторона пишет `String first = getNames().get(0);`. Почему компилятор предупреждает — и какова опасность, если игнорировать предупреждение?
Метод объявляет `public static List getNames() { ... }` (без параметра типа у списка). Вызывающая сторона пишет `String first = getNames().get(0);`. Почему компилятор предупреждает — и какова опасность, если игнорировать предупреждение?
Was this page helpful?