Generics. Часть II

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

 

Дорогу осилит идущий!

Report Page