— Привіт, Аміго!
— Привіт, Дієго!
— Сьогодні ти дізнаєшся все про 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 і замість того, щоб робити повноцінні типи з параметрами, прикрутили хитру оптимізацію. Насправді до типів не додали жодної інформації про їх типи-параметри, а вся магія відбувалася на етапі компіляції.
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’а не зберігається жодної інформації про його тип-параметр. Такий підхід пізніше назвали стиранням типів.
Тобто якщо у тебе є свій клас із типами-параметрами, ти не можеш використовувати інформацію про них усередині класу.
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. Приклад:
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 обов'язково є.
Що ж. Розумно.