Привет! Сегодня мы поговорим о дженериках. Если нужен короткий ответ: дженерик это тип с параметром, то есть класс или метод, которому при объявлении задают, с какими данными он работает. Запись
List<String> означает список, куда компилятор пустит только строки. Появились дженерики в Java 5, и главный их смысл в том, чтобы ошибка с типом всплывала при компиляции, а не через полгода в работающей программе.
Кратко
- Дженерик это класс или метод, которому при создании указывают тип данных.
List<String> умеет работать только со строками, и это проверяет компилятор. - Польза не в удобстве записи, а в раннем обнаружении ошибок: неверный тип просто не скомпилируется, вместо того чтобы уронить программу потом.
- Без дженериков список хранит
Object, поэтому на выходе нужно приведение типов, и любая ошибка обернется ClassCastException уже при выполнении. Raw type это дженерик, у которого убрали параметр типа: List list вместо List<String> list. Он остался в языке ради совместимости с кодом, написанным до Java 5, и в новом коде не нужен.- Типизировать можно и отдельный метод:
<T> перед типом возвращаемого значения делает метод дженерик-методом, и одна и та же логика работает с разными типами. - Свой дженерик-класс пишется так же просто:
public class Box<T>, а дальше T используется внутри как обычный тип.
Надо сказать, что ты выучишь много нового! Дженерикам будет посвящена не только эта, но еще и несколько следующих лекций.
![Три цветных блока, в каждом по четыре одинаковые чашки: в зеленом чайные, в красном кофейные, в фиолетовом стаканы навынос]()
Поэтому, если эта тема тебе интересна, считай, что повезло: сегодня ты узнаешь многое об особенностях дженериков. Ну а если нет, смирись и расслабься! :) Это очень важная тема, и знать ее нужно.
Давай начнем с простого: «что» и «зачем».
Что такое дженерики?
Дженерики это типы с параметром.
При создании дженерика ты указываешь не только его тип, но и тип данных, с которыми он должен работать.
Думаю, самый очевидный пример уже пришел тебе в голову, это
ArrayList! Вот как мы обычно создаем его в программе:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> myList1 = new ArrayList<>();
myList1.add("Test String 1");
myList1.add("Test String 2");
}
}
Как нетрудно догадаться, особенность списка заключается в том, что в него нельзя будет «запихивать» все подряд: он работает исключительно с объектами
String.
Зачем нужны дженерики
Теперь давай сделаем небольшой экскурс в историю Java и попробуем ответить на вопрос: «зачем?». Для этого мы сами напишем упрощенную версию класса ArrayList.
Наш список умеет только добавлять данные во внутренний массив и получать эти данные:
public class MyListClass {
private Object[] data;
private int count;
public MyListClass() {
this.data = new Object[10];
this.count = 0;
}
public void add(Object o) {
this.data[count] = o;
count++;
}
public Object[] getData() {
return data;
}
}
Допустим, мы хотим, чтобы наш список хранил только числа
Integer. Дженериков у нас нет.
Мы не можем явно указать проверку
o instanceof Integer в методе
add(). Тогда весь наш класс будет пригоден только для
Integer, и нам придется писать такой же класс для всех существующих в мире типов данных!
Мы решаем положиться на наших программистов, и просто оставим в коде комментарий, чтобы они не добавляли туда ничего лишнего:
//use it ONLY with Integer data type
public void add(Object o) {
this.data[count] = o;
count++;
}
Один из программистов прозевал этот комментарий и попытался по невнимательности положить в список числа вперемешку со строками, а потом посчитать их сумму:
public class Main {
public static void main(String[] args) {
MyListClass list = new MyListClass();
list.add(100);
list.add(200);
list.add("Lolkek");
list.add("Shalala");
Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
System.out.println(sum1);
Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
System.out.println(sum2);
}
}
Вывод в консоль:
300
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at Main.main(Main.java:14)
Что худшее в этой ситуации?
Далеко не невнимательность программиста. Худшее то, что
неправильный код попал в важное место нашей программы и успешно скомпилировался.
Теперь мы увидим ошибку не на этапе написания кода, а только на этапе тестирования (и это в лучшем случае!).
Исправление ошибок на более поздних этапах разработки стоит намного больше: и денег, и времени.
Именно в этом заключается преимущество дженериков: класс-дженерик позволит незадачливому программисту обнаружить ошибку сразу же. Код просто не скомпилируется!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> myList1 = new ArrayList<>();
myList1.add(100);
myList1.add(100);
myList1.add("Lolkek");//ошибка!
myList1.add("Shalala");//ошибка!
}
}
Программист сразу «очухается» и моментально исправится.
Та же ошибка с обычным ArrayList
Кстати, нам не обязательно было создавать свой собственный класс-
List, чтобы увидеть ошибку такого рода.
Достаточно просто убрать скобки с указанием типа (
<Integer>) из обычного ArrayList!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add(100);
list.add(200);
list.add("Lolkek");
list.add("Shalala");
System.out.println((Integer) list.get(0) + (Integer) list.get(1));
System.out.println((Integer) list.get(2) + (Integer) list.get(3));
}
}
Вывод в консоль:
300
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at Main.main(Main.java:16)
То есть даже используя «родные» средства Java, можно допустить такую ошибку и создать небезопасную коллекцию:
приведение ссылочных типов тут ничем не подстраховано.
Однако, если вставить этот код в IDEA, мы увидим предупреждение: “
Unchecked call to add(E) as a member of raw type of java.util.List”
Нам подсказывают, что при добавлении элемента в коллекцию без дженериков что-то может пойти не так.
Что такое raw type
Но что значит фраза «raw type»?
Дословный перевод будет вполне точным: «
сырой тип» или «
грязный тип».
Raw type это класс-дженерик, из которого удалили его тип.
Иными словами,
List myList1 это
Raw type. Противоположность ему это
generic type, то есть сам класс-дженерик, у которого есть параметр типа. А когда параметр подставлен и получилась конкретная запись вроде
List<String> myList1, ее называют
parameterized type, параметризованный тип.
У тебя мог возникнуть вопрос: а почему в языке вообще позволено использовать
raw types?
Причина проста. Создатели Java оставили в языке поддержку
raw types, чтобы не создавать проблем с совместимостью. К моменту выхода Java 5.0 (в этой версии впервые появились дженерики) было написано уже очень много кода с использованием
raw types.
Поэтому такая возможность сохраняется и сейчас. Как это устроено внутри, разбирается в отдельной лекции про
стирание типов, а формальное описание есть в
документации Oracle про raw types.
Мы уже не раз упоминали классическую книгу Джошуа Блоха «Effective Java» в лекциях.
Как один из архитекторов платформы Java и автор Java Collections Framework, он не обошел в книге и тему использования
raw types и
generic types.
![Обложка книги Джошуа Блоха Effective Java, третье издание]()
Правило 26 этой книги носит весьма красноречивое название:
«Не используйте raw types»
Это то, что нужно запомнить. При использовании классов-дженериков ни в коем случае не превращай
generic type в
raw type. Если свести разницу в одну таблицу, получится так.
| |
List list (raw type) |
List<String> list (generic type) |
Что примет add() |
любой объект, компилятор промолчит |
только String, все остальное не соберется |
Что вернет get() |
Object, приводить тип придется вручную |
сразу String, приведение не нужно |
| Когда всплывет ошибка |
при выполнении, в виде ClassCastException |
при компиляции, прямо в редакторе |
| Что скажет IDE |
предупреждение Unchecked call to add(E) |
ничего, тут все типобезопасно |
| Зачем такая запись в языке |
совместимость с кодом, написанным до Java 5 |
обычный способ писать новый код |
Типизированные методы
Java позволяет тебе типизировать отдельные методы, создавая так называемые generic methods.
Чем такие методы удобны? Прежде всего тем, что позволяют работать с разными типами параметров. Если к разным типам можно безопасно применять одну и ту же логику, дженерик-метод будет отличным решением. Рассмотрим пример.
Допустим, у нас есть какой-то список
myList1. Мы хотим удалить из него все значения, и заполнить все освободившиеся места новым значением.
Вот так будет выглядеть наш класс с дженерик-методом:
public class TestClass {
public static <T> void fill(List<T> list, T val) {
for (int i = 0; i < list.size(); i++)
list.set(i, val);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Старая строка 1");
strings.add("Старая строка 2");
strings.add("Старая строка 3");
fill(strings, "Новая строка");
System.out.println(strings);
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
fill(numbers, 888);
System.out.println(numbers);
}
}
Обрати внимание на синтаксис, он выглядит немного необычно:
public static <T> void fill(List<T> list, T val)
Перед типом возвращаемого значения написано <T>, что указывает на дженерик метод. В данном случае метод принимает на вход 2 параметра: список объектов T и еще один отдельный объект Т.
За счет использования <T> и достигается типизация метода: мы не можем передать туда список строк и число. Список строк и строку, список чисел и число, список наших объектов
Cat и еще один объект
Cat, только так.
В методе
main() наглядно демонстрируется, что метод
fill() легко работает с разными типами данных.
Сначала он принимает на вход список строк и строку, а потом список чисел и число.
Вывод в консоль:
[Новая строка, Новая строка, Новая строка]
[888, 888, 888]
Представь, если бы логика метода
fill() нужна была бы нам для 30 разных классов, и у нас не было бы дженерик-методов.
Мы вынуждены были бы писать один и тот же метод 30 раз, просто для разных типов данных! Но благодаря generic-методам мы можем использовать наш код повторно! :)
Типизированные классы
Ты можешь не только пользоваться представленными в Java дженерик-классами, но и создавать собственные!
Вот простой пример:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Старая строка");
System.out.println(stringBox.get());
stringBox.set("Новая строка");
System.out.println(stringBox.get());
stringBox.set(12345);//ошибка компиляции!
}
}
Наш класс
Box<T> («коробка») является типизированным. Назначив для него при создании тип данных (
<T>), мы уже не сможем помещать в него объекты других типов.
Это видно в примере. При создании мы указали, что наш объект будет работать со строками:
Box<String> stringBox = new Box<>();
И когда в последней строке кода мы пытаемся положить внутрь коробки число 12345, получаем ошибку компиляции!
Вот так просто мы создали свой собственный дженерик-класс! :)
На этом наша сегодняшняя лекция подходит к концу. Но мы не прощаемся с дженериками! В следующих лекциях поговорим о более продвинутых возможностях, поэтому не прощаемся! )
Успехов в обучении! :)
Вопросы и ответы
Что такое дженерики в Java простыми словами?
Это способ сказать классу или методу, с каким типом данных он работает. Вместо просто списка вы объявляете список строк или список чисел, и компилятор с этого момента следит, чтобы туда не попало ничего постороннего. Отсюда и определение: дженерик это тип с параметром.
Зачем нужны дженерики, если можно хранить все как Object?
Хранить можно, но ошибку вы поймаете не там, где ее сделали. Список из
Object спокойно примет и число, и строку, а упадет он позже, при приведении типа, с
ClassCastException. С дженериком тот же неверный элемент просто не даст программе собраться, и вы увидите ошибку сразу в редакторе. Чем позже найдена ошибка, тем дороже она обходится.
Что такое raw type и почему его не стоит использовать?
Raw type это дженерик, записанный без параметра типа:
List list вместо
List<String> list. Компилятор в этом случае теряет всю информацию о типе и перестает вас страховать, а IDE выдает предупреждение об unchecked-вызове. Такая запись оставлена в языке только ради старого кода, написанного до Java 5.
С какой версии Java появились дженерики?
С Java 5.0, она вышла в 2004 году. До нее коллекции хранили
Object, и каждый элемент на выходе приходилось приводить к нужному типу руками. Именно поэтому
raw types и остались в языке: к моменту выхода Java 5 такого кода было написано слишком много, чтобы просто сломать его новой версией.
Как написать свой дженерик-метод?
Перед типом возвращаемого значения ставится параметр типа в угловых скобках, а дальше он используется в сигнатуре как обычный тип:
public static <T> void fill(List<T> list, T val). Такой метод работает с любым типом, но следит за согласованностью: список строк можно заполнить только строкой, список чисел только числом.
Почему нельзя написать List с примитивом внутри?
Параметром дженерика может быть только ссылочный тип, поэтому
List<int> не скомпилируется. Вместо примитива берут класс-обертку:
List<Integer>. Именно поэтому во всех примерах выше стоит
Integer, а не
int. Само число при добавлении в список превращается в объект автоматически.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ