— Привет, Амиго!
— Привет, Элли!
— Сегодня мы с Ришей собираемся рассказать тебе все о 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 обязательно есть.
Что ж. Умно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ