Тема 11. Параметрический полиморфизм - BelyiZ/JavaCourses GitHub Wiki

Содержание:

  1. Что такое Дженерики и зачем они нужны
  2. Cпособ применения Дженериков
    1. Типовой класс
    2. Конструктор
    3. Интерфейс
    4. Метод
  3. Ограничения Дженериков
  4. Список литературы/курсов

Что такое Дженерики и зачем они нужны

"Дженерики" (обобщения) — это особые средства языка Java для реализации обобщённого программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения их описания.

В Java "Дженерики" Java Generics, доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. "Дженерики" (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов.

Рассмотрим пример, в котором вы должны составить список всех, кто имеет отношение к баскетбольной команде. Неважно, тренер, спортсмен, участник группы поддержки или человек, который переносит вещи баскетболистов. В этом случае группировать бы их стали их как "Баскетбольная команда и штаб команды", а не классифицировали их. Точно так же, когда необходимо хранить некоторые данные, для Вас важен контент, а не тип данных, и именно в этом случае необходимо использовать "Дженерики".

Дженерики в Java – это термин, обозначающий набор языковых возможностей, связанных с определением и использованием общих типов и методов. Общие методы Java отличаются от обычных типов данных и методов. До Generics использовалась коллекция для хранения любых типов объектов, то есть "неуниверсальных". Теперь Generics заставляет хранить объекты определенного типа.

Если обратить внимание на классы платформы Java-коллекции, то возможно отметить, что большинство классов принимают параметр / аргумент типа Object. По сути, в этой форме они могут принимать любой тип Java в качестве аргумента и возвращать один и тот же объект или аргумент. Они в основном неоднородны, то есть не похожего типа.

Иногда в приложении Java тип данных ввода не является фиксированным. Входными данными могут быть: целое число, число с плавающей запятой или строка. Чтобы назначить ввод переменной правильного типа данных, необходимо было провести предварительные проверки.

В традиционном подходе после получения ввода проверяется тип данных ввода, а затем назначается переменная правого типа данных. При использовании этой логики длина кода и время выполнения были увеличены. Чтобы избежать этого, были введены "Дженерики".

Когда используется "Дженерики", параметры в коде автоматически проверяются во время компиляции, и он устанавливает тип данных по умолчанию. Так что это то место, где вам нужна концепция обобщений в Java.

Преимущества "Дженериков" в Java

  • Повторное использование кода.

Возможно составить стратегию, класс или интерфейс один раз и использовать их для любого типа или любым другим способом.

  • Кастинг отдельных типов не требуется. По сути, происходит восстановление информации из ArrayList каждый раз, когда нужно ее типизировать. Типирование при каждой необходимости восстановления является серьезной задачей. Чтобы искоренить этот подход, были введены "Дженерики".

  • Реализация неуниверсального алгоритма. "Дженерики" могут рассчитывать алгоритмы, которые работают с различными типами элементов, а также являются безопасными типами.

Соглашение об именовании "Дженериков" Соглашение об именовании помогает нам понимать код и унифицирует его, поэтому ему нужно следовать. В "Дженериках" есть правила именования. Обычно это прописные буквы, что легко отличает их от Java переменных.

Общепринятые правила: E — element, для элементов параметризованных коллекций (широко используется в Java Collections Framework, например, ArrayList, Set и т.д.); K — key, для ключей map-структур; V — value, для значений map-структур; N — number, для чисел; T — type, для обозначения типа параметра в произвольных классах; S, U, V и так далее — применяются, когда в "Дженерик"-классе несколько параметров.

Существует 4 различных способа применения "Дженериков".

  1. Типовой класс
  2. Конструктор
  3. Интерфейс
  4. Метод

Cпособ применения Дженериков

Типовой класс

Класс называется "Дженериком", если он объявляет одну или несколько переменных типа. Эти типы переменных известны как параметры типа класса Java.

Объявляем "Дженерик"-классы: cоздадим баскетбольную команду и группу поддержки.

Создадим класс TeamСommunity, который умеет работать только с элементами определённого типа. Пусть для простоты в этой Команде пока будет только один элемент:

class TeamСommunity<T> { // обозначение типа - T
// переменная с типом T
private T team;

public void putTeamСommunity(T team) { //параметр метода типа T
    this.team = team;
}

public T getTeamСommunity() { // возвращает объект типа T
    return team;
}
}

В классе два метода:

Первый добавляет элемент в сообщество; Второй убирает из сообщества. Во всех случаях, кроме заголовка класса, символ T пишется без угловых скобок, он обозначает один и тот же параметр типа.

Параметром типа для "Дженерика" может быть только ссылочный тип, интерфейс или перечисление (Enum). Примитивные типы и массивы с "Дженериками" не используются, то есть нельзя создать Team<int> или Team<int[]>, но можно — Team<Integer> или Team<List<Integer>>.

Конструктор

(создаем экземпляры)

Конструктор Java – это блок кода, который инициализирует вновь созданный объект. Конструктор напоминает метод экземпляра в Java, но это не метод, поскольку он не имеет возвращаемого типа. Конструктор имеет то же имя, что и класс, и выглядит так в коде Java.

Cоздадим сообщество для членов команды. Игроков-баскетболистов пусть представляет (Класс TeamB).

class TeamB {}
TeamСommunity<TeamB> teamСommunityInTeamB = new TeamСommunity<TeamB>();  

Краткая запись:

TeamСommunity<TeamB> teamСommunityInTeamB = new TeamСommunity<>();

Определение компилятором, что должно находиться дальше в краткой записи (мы опустили <TeamB>) называется type inference — выведение типа, а оператор «<>» — это diamond operator.

"Дженерик"-классы хороши своей универсальностью: с классом TeamСommunity теперь можно создать не только сообщество для игроков, но и, например, сообщество для болельщиков или технического персонала:

class Сheerleading {}
class TechnicalStaff {}
    TeamСommunity<Сheerleading>  teamСommunityInСheerleading = new TeamСommunity<>();
    TeamСommunity<TechnicalStaff>  teamСommunityInTechnicalStaff = new TeamСommunity<>();

Также возможно создать "Дженерик"-класс с двумя параметрами для сообщества с двумя видами его членов.

class TwoTeamСommunity<T, S> {
private T firstteam;
private S secondteam;
//...
}

Предположим, что наше сообщество включает в себя игроков и группу поддержки.

TwoTeamСommunity<TeamB, Сheerleading > TeamBСheerleadingTeamСommunity = new TwoCellsBox<>();

Отметим, что type inference и diamond operator позволяют нам опустить оба параметра в правой части.

Интерфейс

Интерфейс в Java относится к абстрактным типам данных. Они позволяют манипулировать коллекциями Java независимо от деталей их представления. Кроме того, они образуют иерархию в объектно-ориентированных языках программирования.

Объявление "Дженерик"- интерфейсов похоже на объявление "Дженерик"-классов. Продолжим тему спорта и создадим интерфейс Штаба баскетбольной команды Headquarters сразу с двумя параметрами: тип зданий и способ выбора, учитывая формат встречи:

interface Headquarters<T, S> {
    void Meet(T type, S form);
}

Реализовать этот интерфейс можно в обычном, не "Дженерик"- классе:

class MyTeamBMeetMethod {
}

class MyNonGenericTeamBMeet implements Headquarters<TeamB, MyTeamBMeetMethod> {
    @Override
    public void Meet(TeamB type, MyTeamBMeetMethod form) {
// здесь происходит выбор здания способом, учитывая формат встречи, MyTeamBMeetMethod
}
}

Но можно пойти другим путём и сначала объявить "Дженерик"-класс с двумя параметрами:

class HeadquartersImpl<T, S> implements Headquarters<T, S> {
    @Override
    public void  Meet(T type, S form) {
// здесь происходит выбор здания T способом S, учитывая формат встречи
}
}

Или скомбинировать эти два способа и написать "Дженерик"-класс только с одним параметром:

class TeamMeet<T> implements Headquarters<TeamB, T> {
    @Override
    public void Meet(T type, S form) { 
}
}

"Дженерик"-классы и "Дженерик"-интерфейсы вместе называются "Дженерик"-типами.

Можно создавать экземпляры "Дженерик"-типов "без расшифровки", то есть никто не запретит вам объявить переменную типа Box — просто Box:

TeamСommunity teamСommunity = new TeamСommunity<>();

Данная ситуация именуется термином — raw type, то есть "сырой тип". Эту возможность оставили в языке для совместимости со старым кодом, который был написан до появления "Дженериков".

Метод

Универсальные методы очень похожи на универсальные классы. Они отличаются друг от друга только одним аспектом, заключающимся в том, что информация об области действия или типе находится только внутри метода. Универсальные методы вводят свои параметры типа.

В примерах выше были обозначены параметризованные методы в "Дженерик"-классах и интерфейсах. Типизированными могут быть как параметры метода, так и возвращаемый тип.

До этого использовались в методах только те обозначения типов, которые объявлены в заголовке "Дженерик"-класса или интерфейса, но это не обязательно. Предположим, у нашего места встречи есть секретное место для особенных встреч и есть ещё опция: сбор в другом месте. Напишем метод для этого:

interface Headquarters<T, S> {
    void Meet(T type, S form);
    <E> void transfer(E Special);
}

У метода javatransfer есть свой личный параметр для типа, который не обязан совпадать ни с типом T, ни с типом S . При первом упоминании новый параметр, как и в случае с заголовком класса или интерфейса, пишется в угловых скобках.

"Дженериков"-методы можно объявлять и в обычных (не "Дженерик") классах и интерфейсах.

class HeadquartersImpl {

public <T, S> void Meet(T type, S form) {
// здесь происходит выбор здания T способом S, учитывая формат встречи
}
}

Здесь "Дженерики" используются только в методе.

Особенности (синтаксис): параметры типов объявляются после модификатора доступа (public), но перед возвращаемым типом (void). Они перечисляются через запятую в общих угловых скобках.

Ограничения Дженериков

Добавим дополнительное свойство — количество посещенных игр для членов сообщества.

abstract class Team{
    public abstract double getPlay();
}
class TeamB extends Team{
    @Override
    public double getPlay() {
        return 2;
}
}

class Сheerleading extends Team{
    @Override
    public double getWeight() {
        return  1;
}
}

Теперь попробуем использовать эту массу в методе уже знакомого класса Box:

class TeamСommunity<T> {
private T team;
public double getTeamPlay() {
// не скомпилируется
    return team == null ? 0 : team.getPlay();
}
//... остальные методы
}

И получим ошибку при компиляции: компилятору не знает, что T — это часть сообщества. Исправим это с помощью так называемого upper bounding — ограничения сверху:

class TeamСommunity<T extends Team> {
// методы класса
}

Теперь метод getTeamPlay успешно скомпилируется.

Здесь T extends Team означает, что в качестве T можно подставить Team или любой класс-наследник Team. Из уже известных нам классов это могут быть, например, TeamB или Сheerleading. Так как и у Team, и у всех его наследников есть метод getPlay, его можно вызывать в новой версии "Дженерик"-класса TeamСommunity.

Для одного класса или интерфейса можно добавить сразу несколько ограничений. Вспомним про интерфейс для штаб-квартиры команды и введём класс для метода встречи — MeetMethod. Тогда Headquarters можно переписать так:

class MeetMethod {
}

interface Headquarters<T extends Team, S extends MeetMethod> {
    void Meet(T type, S form);
}

В качестве ограничения может выступать класс, интерфейс или перечисление (Enum), но не примитивный тип и не массив. При этом для интерфейсов тоже используется слово extends, а не implements: <T extends SomeInterface> вместо <T implements SomeInterface>.

Wildcards

До этого мы использовали для параметров типов буквенные имена, но в Java есть и специальный символ для обозначения неизвестного типа — "?". Его принято называть wildcard, дословно — "дикая карта".

Wildcard нельзя подставлять везде, где до этого введены буквенные обозначения. Не получится, например, объявить класс Box<?> или "Дженерик"-метод, который принимает такой тип:

class Box<?>{ // не скомпилируется       
   ? variable;  // не скомпилируется
   public <?> void someMethod(? param){ // не скомпилируется
//...   
}
}

Wildcards удобно использовать для объявления переменных и параметров методов совместно с классами из Java Collection Framework.

В примере ниже мы можем подставить вместо "?" любой тип, в том числе TeamB, поэтому строка успешно скомпилируется:

List<?> Ex1 = new ArrayList<TeamB>();

Wildcards можно применять для ограничений типов:

List<? extends Team> Ex2 = new ArrayList<TeamB>();

Это ограничение сверху, upper bounding, — вместо "?" допустим Garbage или любой его класс-наследник, то есть TeamB подходит.

Но можно ограничить тип и снизу. Это называется lower bounding и выглядит так:

List<? super Team> Ex3 = new ArrayList<Team>();

Здесь <? super Team> означает, что вместо "?" можно подставить Team или любой класс-предок Team. Все ссылочные классы неявно наследуют класс Object, так что в правой части ещё может быть ArrayList<Object>.

Список литературы/курсов

  1. https://java-blog.ru/osnovy/interfeysy - Интерфейсы в Java
  2. https://docs.oracle.com/javase/tutorial/java/generics/types.html - набор рекомендаций , когда какие обозначения лучше использовать в "Дженериках"
  3. https://java-blog.ru/osnovy/generics-java
  4. https://javascopes.com/java-generics-d3763e32/
  5. https://javadevblog.com/rukovodstvo-po-rodovy-m-tipam-v-java-java-generics-opisanie-i-primery.html

Тема 10. Полиморфизм | Оглавление | Тема-12. Enum Перечисления

⚠️ **GitHub.com Fallback** ⚠️