Generics. Часть I
Дорогу осилит идущийЕсли кто-то обратил внимание, в статье про полиморфизм было указано, что в Java он представлен в трех видах. Об одном из них – параметрическом полиморфизме, а точнее, о синтаксисе, который Java для него предоставляет, мы будем говорить сегодня и завтра. Урок разбит на две части.
Итак, generic (дженерик, обобщенный тип) – средство языка, позволяющее обрабатывать данные разных типов. Именно на базе generic’ов работают коллекции в Java, с которыми уже знакомы некоторые из вас. Также на обобщенных типах построено функциональное программирование в Java, с которым мы познакомимся в дальнейшем.
Иными словами, дженерики предоставляют возможность описывать классы, их поля и методы, не указывая конкретный тип данных (полей, параметров или возвращаемых значений). Это нужно тогда, когда тип данных не имеет значения при обработке (или имеет, но лишь отчасти, об этом – ниже, а также в следующей части урока).
Класс, работающий с generic’ом, называется параметризованным.
В качестве базиса урока по обобщенным типам (включая завтрашний) можно использовать статью https://metanit.com/java/tutorial/3.11.php
Но советую обратиться к ней уже после прочтения текущей.
Синтаксис обобщенных типов
Рассмотрим синтаксис обобщенного класса и пример создания его экземпляра:
//Параметризуем класс generic T. T может быть любым ссылочным типом
public class Generic1<T> {
//Объявляем поле типа T. В конечном итоге для каждого экземпляра T будет соответствовать конкретному типу данных
private T field;
//Метод, возвращающий объект, соответствующий типу T для данного экземпляра Generic1
public T getField() {
return field;
}
//Метод, принимающий параметр, соответствующий типу T для данного экземпляра Generic1
public void setField(T field) {
this.field = field;
}
}
...
// Создаем объект Generic1, T в этом экземпляре равносильно String
Generic1<String> generic1String = new Generic1<>();
generic1String.setField("1");
// Создаем объект Generic1, T в этом экземпляре равносильно Integer
Generic1<Integer> generic1Integer = new Generic1<>();
generic1Integer.setField(1);
// Создаем объект Generic1 без указания параметризации, T в этом экземпляре равносильно Object
Generic1 generic1Object = new Generic1();
// Поскольку тип не указан явно, мы можем передавать все, что может быть приведено к ссылочному типу
generic1Object.setField("1");
//Здесь работает принцип автоупаковки. Будет передан параметр типа Integer, а не int
generic1Object.setField(1);
В данном примере параметризованный тип указан как T. Это распространенная практика (T – сокращение от Type), но в целом, T можно заменить на любое другое название. Например, во внутренних классах Java часто встречается R – Result.
Иногда параметризованные типы обозначают не сокращением, а полным наименованием. Общепринятые правила обозначения для таких случаев, по сути, отсутствуют. Вы можете встретить как формат наименований как у констант, так и такой же, как у классов:
public class Generic1<ORIGINAL_NAME> {…}
public class Generic1<OriginalName> {…}
Важно отметить, что параметризовать можно не только классы, но и интерфейсы. Синтаксис будет аналогичным.
При обращении к параметризованным полям или параметрам будут доступны только методы Object.
Также класс может содержать несколько параметризованных типов. Для демонстрации передачи двух параметризованных параметров используется конструктор, для методов принцип тот же:
//Параметризуем класс типомами T1 и T2. Они могут быть любыми ссылочными типами
public class Generic2<T1, T2> {
private T1 field1;
private T2 field2;
public Generic2(T1 field1, T2 field2) {
this.field1 = field1;
this.field2 = field2;
}
public T1 getField1() {
return field1;
}
public void setField1(T1 field1) {
this.field1 = field1;
}
...
}
...
Generic2<String, Integer> generic2StrInt = new Generic2<>("1", 1);
Generic2<String, String> generic2StrStr = new Generic2<>("", "");
Как видите, в коде часто используется оператор <>, содержащий (или не содержащий) внутри себя тип данных. Именно он однозначно указывает на то, что мы имеем дело с обобщенным типом.
Ограничения обобщенных типов
В ряде случаев нам может понадобиться создать обобщенный тип, который будет работать только для какой-то части классов, а не для всех. Для этого мы можем использовать ключевое слово extends при объявлении классов, внутри <>:
public class Generic1<T extends Number> {…}
Для такого класса можно создать объекты, параметризуя его только Number или его наследниками – Integer, Double и др. После extends можно использовать как классы, так и интерфейсы.
При обращении к параметризованному полю или методу в таком случае будут также доступны поля и методы его предка. Например, для T extends Number будут доступны методы абстрактного класса Number.
Способы создания объектов обобщенных типов и немного об обратной совместимости в Java
В рамках примеров выше мы создавали объекты обобщенных типов двумя разными способами:
- Generic1<Integer> generic1Integer = new Generic1<>(). <> - Diamond (алмазный оператор). Синтаксис, позволяющий не указывать тип дженерика повторно. Появился в Java 7. Ранее этот код выглядел бы как Generic1<Integer> generic1Integer = new Generic1<Integer>(). Алмазным оператором называется именно использование <> без какого-либо значения внутри;
- Generic1 generic1Object = new Generic1(). Такая форма записи называется Raw type (сырой тип). В абсолютном большинстве случаев так делать не стоит, потому что мы лишаем себя проверки типов со стороны Java.
В целом, второй пункт объясняет, зачем вообще нужны обобщенные типы и почему бы вместо них не использовать поля и параметры типа Object. Это тоже рабочий подход (и именно так и были написаны первые коллекции вроде Vector, когда обобщенных типов в Java еще не было). Но он заставляет при каждом вызове метода проверять тип объекта, который передается как параметр. Кроме того, вместо использования T (или другого обозначения типа) в каждом методе пришлось бы указывать Object. Это не слишком удобно даже если класс параметризован одним типом. А если параметризован несколькими – работа с данным классом превращается в ад.
Также важно понимать, что функционал обобщенных типов в Java появился не сразу, а лишь в Java 5. Почему это имеет значение? Потому что Java поддерживает обратную совместимость кода. И не сделай ее, всю псевдопараметризацию, о которой упомянуто выше, пришлось переводить на новые рельсы. Согласитесь, мало кто захочет переписывать половину проекта, потому что с обновлением версии языка изменился синтаксис для работы с классами, используемыми в проекте. Это причина, почему raw type в принципе существует в Java.
Другим следствием обратной совместимости явилось то, что мы можем параметризовать класс лишь ссылочными типами, а для примитивов параметризация недоступна (отсюда и растут ноги классов-оберток, точнее их актуальности в современной Java. Созданы они были еще для псевдопараметризации).
Также стоит учитывать, что при компиляции кода параметризация стирается, в итоге условный Generic1<Integer> generic1Integer превращается для Java в Generic1 generic1Integer. Это поведение получило название «стирание типов» и оно также является следствием обратной совместимости. Данная информация имеет не так много практического применения, но про это вполне могут спросить на собеседовании.
Проверки типов
Мы знакомы с проверками типов с помощью instanceof и через объект класса Class. Пару нюансов об их использовании для обобщенных типов:
- При вызове getClass() у двух элементов, одного типа, но параметризованных разными типами, будет возвращен один и тот же объект Class;
- Generic1<String>.class - ошибка компиляции. Обращение к литералу класса всегда происходит без указания параметризации: Generic1.class;
- instanceof также стоит использовать без указания параметризованного типа. Если его указать, то условие с таким instanceof будет всегда true (если у объекта и проверяемого класса параметризованный тип совпадает), либо будет ошибка компиляции (если параметризованный тип неизвестен или точно не совпадает с таковым у проверяемого объекта). Оба варианта бесполезны. Можете убедиться в описанном поведении на практике на практике, написав проверки для обобщенных классов через instanceof.
С теорией на сегодня все!
В следующем уроке мы разберемся с другой функциональностью, которая доступна при параметризации – Wild card – и узнаем, для чего в Java нужен оператор «?».

Переходим к практике:
Задача 1:
Создать обобщенный тип, принимающий в себя любого из наследников Number. Создать метод, возводящий значение параметризованного типа в степень, переданную параметром в метод.
Задача 2:
Создать класс-обертку над объектом любого типа. Предусмотреть boolean-метод, проверяющий значение объекта на null.
Задача 3:
Реализовать класс для работы с массивом. Разработать метод, производящий поиск значения в массиве. Если значение не найдено — выбрасывать исключение. Если найдено — возвращать его.
Задача 4(*):
Реализовать параметризованный класс, хранящий и обрабатывающий стек. Стек — структура данных, в котором каждый элемент хранит ссылку на следующий. Работает по принципу LIFO (последний вошел — первый вышел).
Реализовать следующие методы:
- Добавление элемента в стек;
- Удаление элемента из стека. При удалении несуществующего элемента – исключение;
- Получение глубины (количества элементов) стека;
- Поиск по стеку, при отсутствии искомого значения – исключение;
- Получение строкового эквивалента элементов стека, представленных в виде массива ([строковое представление элемента1, ..., строковое представление элементаN]).
Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/+relA0-qlUYAxZjI6
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!