— Привет, Амиго!

— Привет, Элли!

— Сегодня мы с Ришей собираемся рассказать тебе все о generic’ах.

— Так я уже вроде бы почти все знаю.

— Почти все, да не все.

— Да? Ладно, я готов слушать.

— Тогда начнем.

Generic’ами в Java, называют классы, которые содержат типы-параметры.

Причины появления generic’ов – см. комментарий в коде:

Пример
ArrayList stringList = new ArrayList();
stringList.add("abc"); //добавляем строку в список
stringList.add("abc"); //добавляем строку в список
stringList.add( 1 ); //добавляем число в список

for(Object o: stringList)
{
 String s = (String) o; //тут будет исключение, когда дойдем до элемента-числа
}

Как решают проблему Generic’и:

Пример
ArrayList<String> stringList = new ArrayList<String>();
stringList.add("abc"); //добавляем строку в список
stringList.add("abc"); //добавляем строку в список
stringList.add( 1 ); //тут будет ошибка компиляции

for(Object o: stringList)
{
 String s = (String) o;
}

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

— Я это уже знаю.

— Вот и отлично. Повторение — мать учения.

Но разработчики Java немного схалявили при создании Generic и вместо того, чтобы делать полноценные типы с параметрами прикрутили хитрую оптимизацию. На самом деле в типы не добавили никакой информации о их типах-параметрах, а вся магия происходила на этапе компиляции.

Код с generic’ами
List<String> strings = new ArrayList<String>();
strings.add("abc");
strings.add("abc");
strings.add( 1); // тут ошибка компиляции

for(String s: strings)
{
 System.out.println(s);
}
Что происходит на самом деле
List strings = new ArrayList();

strings.add((String)"abc");
strings.add((String)"abc");
strings.add((String) 1); //ошибка компиляции

for(String s: strings)
{
 System.out.println(s);
}

— Хитро, да.

— Да, но у этого подхода есть побочный эффект. Внутри класса-generic’а не хранится никакой информации о его типе-параметре. Такой подход позже назвали стиранием типов.

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

Код с generic’ами
class Zoo<T>
{
 ArrayList<T> pets = new ArrayList<T>();

 public T createAnimal()
 {
  T animal = new T();
  pets.add(animal)
  return animal;
 }
}
Что происходит на самом деле
class Zoo
{
 ArrayList pets = new ArrayList();

 public Object createAnimal()
 {
  Object animal = new ???();
  pets.add(animal)
  return animal;
 }
}

При компиляции все типы параметров заменяются на Object. И информации о типе, который в него передавали, в классе нет.

— Да, согласен, это не очень хорошо.

— Ладно, все не так страшно, я тебе позже расскажу, как эту проблему научились обходить.

Но и это еще не все. Java позволяет задать тип-родитель для типов-параметров. Для этого используется ключевое слово extends. Пример:

Код с generic’ами
class Zoo<T extends Cat>
{
 T cat;

 T getCat()
 {
  return cat;
 }

 void setCat (T cat)
 {
  this.cat = cat;
 }

 String getCatName()
 {
  return this.cat.getName();
 }
}
Что происходит на самом деле
class Zoo
{
 Cat cat;

 Cat getCat()
 {
  return cat;
 }

 void setCat(Cat cat)
 {
  this.cat = cat;
 }

 String getCatName()
 {
  return this.cat.getName();
 }
}

Обрати внимание на два факта.

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

Во-вторых, в классе Zoo у переменных типа T теперь можно вызвать методы класса Cat. Почему – объяснено в столбце справа (потому что вместо типа T везде подставится тип Cat)

— Ага. Если мы сказали, что в качестве типа-параметра у нас передают тип Cat или его наследников, значит, мы уверены, что методы класса Cat у типа T обязательно есть.

Что ж. Умно.