Введение в обобщения 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>. Оба компилируются; безопасным является только параметризованный.
Сырая версия падает в середине итерации, потому что цикл доверял приведению, которому не следовало. Обобщённая версия сделала ту же ошибку невыразимой — неудачный add(42) в принципе не скомпилируется. Этот переход от времени выполнения к времени компиляции и есть главная причина существования обобщений.
Что рассматривается в этой части книги
Остальные главы этой части разбирают обобщения по частям:
- Обобщённые классы — параметр типа на уровне класса, который вы только что видели, более подробно.
- Обобщённые методы — методы, которые вводят собственный параметр типа, независимый от класса.
- Обобщённые интерфейсы — проектирование контрактов API, параметризованных по типу.
- Ограниченные параметры типа — указание «T должен расширять
Number», чтобы можно было вызывать методы наT. - Wildcards —
? extends T,? super Tи правило PECS, определяющее, когда использовать каждый. - Стирание типов — как JVM реализует обобщения под капотом и почему некоторые ожидаемые вещи не работают.
- Ограничения — каталог действий, которые язык запрещает, с объяснением причин.
Читайте по порядку — каждая глава предполагает знакомство с предыдущими.
Что дальше
Начните с самой распространённой формы — класс, поля и методы которого параметризованы по типу, выбранному вызывающей стороной. Перейдите к Java Generic Classes.