Введение в обобщения Java
Зачем нужны обобщения Java — типобезопасность, повторное использование кода и устранение приведений в коллекциях и API.
Введение в обобщения Java
Обобщения (generics) — это возможность, позволяющая классу, интерфейсу или методу работать с неуказанным типом, а компилятору затем зафиксировать этот тип в том месте, где вы его используете. List<String> — это список строк, компилятор это знает, и любая попытка положить туда Date отклоняется ещё до того, как программа запустится. До появления обобщений в Java 5 тот же список был List из Object, и каждое чтение из него требовало написанного вручную приведения, которое во время выполнения могло удаться, а могло и нет. Обобщения превратили эту азартную игру времени выполнения в проверку времени компиляции, и почти каждый современный Java API сформирован ими.
Проблема, которую решают обобщения
Чтобы понять, зачем существуют обобщения, представьте 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Ошибка проявляется при чтении, далеко от записи. Приведение лжёт — оно говорит «это есть String», и 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 для «ключа» и «значения» map, 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 — после того, как мы охватим достаточно механизмов, чтобы правила обрели смысл.
Разобранный пример: типобезопасность против сырых типов, бок о бок
Программа ниже строит один и тот же контейнер дважды — один раз как сырой List (форма до обобщений) и один раз как List<String>. Оба компилируются; безопасен только параметризованный.
Сырая версия рушится посреди итерации, потому что цикл доверился приведению, которому доверять было не за что. Обобщённая версия сделала ту же ошибку непредставимой — плохое add(42) вообще не скомпилируется. Этот сдвиг от времени выполнения к времени компиляции и есть вся причина, по которой существуют обобщения.
Что охватывает эта часть книги
Остальные главы этой части разбирают обобщения по одной части за раз:
- Обобщённые классы — параметр типа уровня класса, который вы только что видели, подробнее.
- Обобщённые методы — методы, вводящие собственный параметр типа, независимо от класса.
- Обобщённые интерфейсы — проектирование контрактов API, параметризованных по типу.
- Ограниченные параметры типа — указание «T должен расширять
Number», чтобы можно было вызывать методы наT. - Подстановочные знаки (wildcards) —
? extends T,? super Tи правило PECS, решающее, когда какой использовать. - Стирание типов — как JVM реализует обобщения под капотом и почему некоторые ожидаемые вещи не работают.
- Ограничения — каталог того, что язык отказывается вам позволять, с причинами за каждым.
Читайте их по порядку — каждая глава предполагает предыдущие.
Что дальше
Начните с самой распространённой формы — класса, поля и методы которого параметризованы по типу, выбираемому вызывающим. Продолжайте к Обобщённым классам Java.
Practice
A method declares `public static List getNames() { ... }` (no type parameter on the list). The caller writes `String first = getNames().get(0);`. Why does the compiler warn — and what's the danger if you ignore the warning?