Дженерики.

20-01-2020

Дженерики – это параметризованные типы. С их помощью можно объявлять классы, интерфейсы и методы, в которых тип данных указан в виде параметра.

Дженерики похожи на шаблоны в C++ (но имеют существенные отличия). Дженерики поддерживают абстрагирование типов. Разработчики классов должны проектировать классы с использованием дженериков, в то время как пользователи этих классов должны указывать конкретный тип при создании объектов или вызове методов.

Так, мы знакомы с передачей аргументов в методах. Аргументы размещаются в круглых скобках () и передаются в метод. В дженериках вместо передачи аргументов передается тип информации, указанный внутри угловых скобок <>.

Первоначально дженерики использовались для абстрагирования типов при работе с коллекциями.

Введение во фреймворк «Коллекции»

В Java имеется возможность использования массива для хранения элементов одного типа как одного из базовых типов или объектов. Однако массив не поддерживает динамическое распределение памяти–он имеет фиксированную длину,которая не может быть изменена, будучи однажды заданной. Массив является простой линейной структурой. Многие приложения могут потребовать более сложных структур данных, таких как связный список, стек, множество или деревья. В Java динамически распределенные структуры данных, такие как ArrayList, LinkedList, Vector, Stack, HashSet, HashMap, Hashtable, поддерживаются единой архитектурой, которая называется «Фреймворк Коллекции», определяющей общее поведение всех классов, входящих в коллекции.

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

В Java фреймворк «Коллекции» предлагает единый интерфейс для хранения, извлечения и действий с элементами коллекции независимо от лежащей в основе их фактической реализации. В Java пакет фреймворка «Коллекции» (java.util) содержит:

  1. Набор интерфейсов.
  2. Классы реализаций.
  3. Алгоритмы (например, сортировки или поиска).

Первоначально структуры данных Java состояли из array, Vector, и Hashtable, которые были описаны неунифицированным способом. Впоследствии был введен единый фреймворк «Коллекции», в соответствии с которым были модифицированы классы (Vector и Hashtable).

В JDK 1.5 были введены дженерики, которые поддерживают передачу типов и добавляют такие преимущества, как, например, автобоксинг (англ. autoboxing – упаковка – обычно используется без перевода) и анбоксинг (англ. unboxing – распаковка – также обычно используется без перевода), усовершенствование цикла for. Коллекции модернизированы для поддержки дженериков и используют все предоставляемые ими преимущества.

Коллекции и небезопасность типов

Подход к коллекциям до JDK 1.5 имеет следующие недостатки:

  1. Апкастинг к java.lang.Object выполняется непосредственно компилятором. Но программист должен точно провести даункастинг от Object к исходному классу.
  2. Компилятор не способен во время компиляции проверить, выполнен ли даункастинг правильно. Неправильно выполненный даункастинг будет отражен только во время выполнения в виде генерации исключения ClassCastException. Это известно как динамическое связывание, или позднее связывание. Например, если вы случайно добавили объект Integer в приведенный выше список, который предназначен для удерживания объектов типа String, то ошибка отобразится, только когда вы попытаетесь провести даункастинг Integer к String, т.е. во время выполнения.

Введение в дженерики

Дженерики позволяют передавать тип информации компилятору в форме <тип>. Таким образом, компилятор может выполнить все необходимые действия по проверке типов во время компиляции, обеспечивая безопасность по приведению типов во время выполнения.

Например, следующее утверждение с дженериками List<String> (читается как List of Strings) и ArrayList<String> (читается как ArrayList of Strings) информирует компилятор, что List and ArrayList должны удерживать объекты String: List<String> lst = new ArrayList<String>(); ( читается как List of Strings, ArrayList of Strings.)

Известно, как передаются аргументы в методах. Для этого аргументы помещаются внутри круглых скобок () и таким образом передаются в метод. В дженериках вместо передачи аргументов компилятору передается тип информации, заключив его в угловые скобки <>.

Дженерик-классы

Начиная с версии JDK 1.5 вводятся так называемые дженерики для разрешения данной проблемы. Дженерики позволяют абстрагировать типы. Можно разработать класс с дженерик-типом и указать конкретный тип во время инициализации объекта. Компилятор будет в состоянии выполнить необходимую проверку типов во время компиляции и гарантировать, что никакая ошибка по приведению типов не будет иметь места во время выполнения. Это известно как типобезопасность. Формальные параметры типа в описании класса имеют ту же цель, что и формальные параметры в описании метода. Класс может использовать формальные параметры типа для получения информации о типе при создании объекта данного класса. Фактические параметры, используемые во время инициализации, называются фактическими параметрами типа.

Соглашение об именах формальных параметров типа

Рекомендуется использовать одну большую букву для формальных параметров типа.

Например:

  • <E> для элемента коллекции;
  • <T> для типа;
  • <K, V> для ключа и значения;
  • <N> для числа
  • S, U, V и т.д. для 2-го, 3-го, 4-го параметров типа.

Пример дженерик-класса

В данном примере описан класс GenericBox, который имеет параметр типа E, который удерживает переменную content типа E. Конструктор, геттер и сеттер работают с параметром типа E. Метод toString() отображает фактический тип параметра переменной content.

public class GenericBox<E> {
  private E content;

  public GenericBox(E content) {
    this.content=content;
  }
  
  public E getContent(){
     return content;
  }

  public void setContent(E content){
    this.content=content;
  }

  public String toString(){
    return content + "("+content.getClass()+")";
  }

}

Следующая тестирующая программа создает объекты класса GenericBox с различными типами переменных (String, Integer и Double). Обратите внимание, что в JDK 1.5 также введены автобоксинг и анбоксинг для преобразования объектов базового типа и объектов класса оболочек.

public static void main(String[] arg) {

	GenericBox<String> box1=new GenericBox<String>("Hello!");
	String str=box1.getContent(); 
	System.out.println(box1);
	
	
	GenericBox<Integer> box2=new GenericBox<Integer>(12345);
	int i=box2.getContent(); 
	System.out.println(box2);
   
	GenericBox<Double> box3=new GenericBox<Double>(12.345);
	double d=box3.getContent(); 
	System.out.println(box3);

}

Потеря типа Из предыдущего примера видно, что компилятор заменял параметр типа E фактическим типом (таким, как String, Integer) во время инициализации объектов. В этом случае компилятору потребуется создавать новый класс для каждого фактического типа. В действительности компилятор заменяет все ссылки на параметр типа E на Object, выполняет проверку типов и добавляет требуемую операцию даункастинга.

Дженерик-методы

Методы также могут быть определены с дженерик-типами (аналогично дженерик-классу). Например,

public static <E> void ArrayToArrayList(E[] a, ArrayList<E> lst){
  for(E e:a) lst.add(e)
}

Дженерик-метод может объявить параметры формального типа (например, <E>, <K,V>) с предшествующим указанием возвращаемого типа. Параметры формального типа могут быть затем использованы как средства для удержания возвращаемого типа, параметров методов и локальных переменных в теле дженерик-метода для правильной проверки типов компилятором.

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

Дженерики дают возможность использовать различный синтаксис для указания типа в дженерик-методах. Можно указать фактический тип в угловых скобках <>, между оператором «точка» (.) и именем метода. Например, TestGenericMethod.<Integer>ArrayToArrayList(intArray,lst);

Синтаксис делает код лучше читаемым и дает возможность контроля посредством дженерик-типа в ситуациях, когда тип может быть неочевидным.

Wildcards – подстановочные символы

Обобщенный подстановочный символ <?>

В дженериках предоставляется обобщенный символ (wildcard) (?), который применим к любому неизвестному типу. Если мы хотим, чтобы дженерик-метод работал со всеми типами данных, может быть использован обобщенный подстановочный символ.

Дженерики, ограничивающие тип

Ограничивающие параметры типа – это дженерик-тип, который указывает ограничения на дженерик в форме , например, принимает Number и его подклассы, такие как Integer and Double.

Литература

  1. О.И. Гуськова, "ООП в Java", Москва, 2018