Generics. Часть I

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 часто встречается RResult.

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

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

В рамках примеров выше мы создавали объекты обобщенных типов двумя разными способами:

  1. Generic1<Integer> generic1Integer = new Generic1<>(). <> - Diamond (алмазный оператор). Синтаксис, позволяющий не указывать тип дженерика повторно. Появился в Java 7. Ранее этот код выглядел бы как Generic1<Integer> generic1Integer = new Generic1<Integer>(). Алмазным оператором называется именно использование <> без какого-либо значения внутри;
  2. 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. Добавление элемента в стек;
  2. Удаление элемента из стека. При удалении несуществующего элемента – исключение;
  3. Получение глубины (количества элементов) стека;
  4. Поиск по стеку, при отсутствии искомого значения – исключение;
  5. Получение строкового эквивалента элементов стека, представленных в виде массива ([строковое представление элемента1, ..., строковое представление элементаN]).


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/+relA0-qlUYAxZjI6

Мой тг: https://t.me/ironicMotherfucker

 

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

Report Page