Generics. Часть II
Дорогу осилит идущийПараметризация методов
В прошлом уроке мы писали параметризованные методы в рамках параметризованного класса. Но что делать, если необходимо создать параметризованный метод вне generic-класса?
В таком случае нам нужно явно указать методу, что он параметризован. Сделать это можно так:
private <T> void doSth() {
//sth logic
}
Как видим, достаточно указать обобщенный тип перед возвращаемым типом метода. Метод doSth() является параметризованным. Но в таком виде мы не можем полноценно работать с типом, подставляемым в T – у нас нет ни возвращаемого значения типа T, ни параметров этого же типа. Но данный пример хорошо демонстрирует саму параметризацию метода в общем виде. К тому же, даже в таком виде код не содержит ошибок, программа с таким методом скомпилируется корректно.
Однако рассмотрим более «живой» пример. Для дополнительной демонстрации возможностей параметризуем метод двумя типами:
private <T, R> R doSth(T param) {
//sth logic returning R-type object
}
В данном случае, мы указываем, что метод параметризован двумя типами: T и R. Метод возвращает объект типа R, а также принимает параметр типа T. R является любым типом, отличным от T, который неизвестен на этапе компиляции. На практике нам проблематично реализовать тело подобного метода, потому что мы не знакомы с лямбда-выражениями, при использовании которых такая форма записи имела бы смысл. Поэтому сейчас просто отложим в голове, что так делать можно и у подобной параметризации есть своя сфера применения.
Главное, что стоит вынести из этого подраздела – мы можем параметризовать методы вне параметризованного класса (или дополнительно параметризовать прямо в классе-дженерике, тогда нам будет доступна и параметризованный тип уровня класса, и параметризованный тип уровня метода). Можем определенным образом работать с параметризованными параметрами и возвращать значение параметризованного типа.
В качестве закрепления рассмотрим несколько примеров, которые мы могли бы использовать уже сейчас:
// Пример работы с параметризованным параметром метода, при наличии других параметров
private <T> String getStringValue(T param, String prefix) {
return prefix + param.toString();
}
// Пример использования ограничения типа при параметризации метода
private <T extends Number> String getDoubleStringValue(T param) {
return String.valueOf(param.doubleValue());
}
// Возвращение параметризованного типа из метода
private <T> T doSth(T param) {
//какая-то логика обработки параметра
return param;//или другой объект того же типа
}
Примеры, безусловно, исключительно демонстративные, логика внутри методов не несет полезной нагрузки.
Приведение параметризованных типов
Разбирая в прошлом уроке синтаксис обобщенных типов, мы опустили одну небольшую, но важную деталь. Для параметризованных типов не работает приведение в том виде, в котором мы привыкли:
Generic1<String> generic1String = new Generic1<>(); Generic1<Object> generic1Object = generic1String; //Ошибка компиляции
Связано это с тем, что Generic1<String> не является наследником Generic1<Object>. Что делать, если подобное приведение нам необходимо, мы рассмотрим ниже.
Wildcards
В ряде случаев нет необходимости полноценно параметризовать метод или указывать тип при создании переменной параметризованного класса. В таком случае нам может прийти на помощь подстановочный символ «?»:
Generic1<?> generic1 = new Generic1<String>();
В данном случае мы не указываем явно, каким типом данных параметризован экземпляр generic1. В таком виде мы можем записать в эту переменную объект Generic1, параметризованный любым типом. Например, следующей же строчкой мы можем присвоить другое значение этой же переменной:
generic1 = new Generic1<Integer>();
В данной форме записи «?» эквивалентен Object. Условно говоря, если все классы в Java наследуются от Object, то все параметризованные типы наследуются от класса, параметризованного «?». Т.е. Generic1<String> – наследник Generic1<?>. Это не соответствует действительности с точки зрения JVM, но такая аналогия позволяет понять нюансы приведения обобщенных типов.
Другое использование подстановочного символа заключается в передаче в метод параметра обобщенного типа или возврат объекта такого типа из метода. Например, вне класса Generic1, не имея доступ к T, мы можем описать следующие методы:
private Generic1<?> getGeneric(String param) {
return new Generic1<>(param);
}
private Object getValue(Generic1<?> value) {
return value;
}
В англоязычной литературе и, соответственно, в профессиональном сленге подстановочный символ называется wildcard (дословно – подстановочный знак или подстановочный тип).
Справедливости ради, в русскоязычной среде под wildcard зачастую подразумевают не подстановочный символ сам по себе, а объект дженерика, использующий подстановочный символ.
Плюс wildcard’а в том, что мы можем использовать его внутри метода, не параметризуя сам метод. Его минус – это не самостоятельный тип, в отличии от условного «T». Т.е. мы можем использовать вайлдакрд внутри <>, однако объявить переменную или передать параметр типа ? у нас не получится:
private void doSth(? param) {} //ошибка компиляции
…
? obj = new Object(); // ошибка компиляции
Как и при классической параметризации, wildcard можно ограничивать:
- Generic<? extends SthClass>: под такую форму записи подойдут объекты Generic, параметризованные типом SthClass или его наследниками;
- Generic<? super SthClass>: под такую форму записи подойдут объекты Generic, параметризованные типом SthClass или каким-либо из его предков. Ограничение с помощью super доступно только для wildcard. Если кому-то интересно, зачем такое ограничение существует – рекомендую ознакомиться с правилом PECS (Producer Extends Consumer Super).
Wildcard, у которого описано ограничение типа называют Bounded (ограниченный) wildcard. Соответственно, wildcard без ограничения типа – Unbounded (неограниченный) wildcard.
К сожалению, актуальность вайлдкардов достаточно тяжело понять до знакомства с коллекциями. Поэтому данный урок больше направлен на знакомство с синтаксисом, нежели на практики использования. Однако мы обязательно вернемся к этой теме при знакомстве с коллекциями и в других темах (например, Stream API), сравнив использование параметризованных методов и вайлдкардов.
Если вы знаете хорошие примеры применения wildcard на основе пройденных тем – не стесняйтесь кидать их в комменты. С вашего согласия, они будут добавлены в следующую редакцию статьи, если окажутся достаточно наглядными.
С теорией на сегодня все!

Переходим к практике:
Задача 1:
Реализуйте обобщенный тип, хранящий параметризованное поле. Также в классе Main реализуйте параметризованый метод, принимает первым параметром объект вашего дженерика, вторым — объект типа, которым параметризован объект первого параметра. Метод должен возвращать значение поля дженерика, если оно != null, в противном случае — возвращать второй параметр.
Задача 2:
Используя Задачу 1 из урока Generics. Часть I, реализуйте в Main метод, принимающий аргументом объект подходящего для дженерика типа и возвращающий объект дженерика. Допустима параметризация только с использованием wildcard.
Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/+relA0-qlUYAxZjI6
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!