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

— Привіт, Дієго!

— Сьогодні ти дізнаєшся все про 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 обов'язково є.

Що ж. Розумно.