Если вы не используете дженерики, то все объекты принимаются как Object. Поэтому там, где нам нужен конкретный тип, а не Object, нам нужно делать явное приведение типа данных каждый раз, когда возвращаем элементы.
Другой пример - коллекция принимает список объектов, этот список хранит не только String, но и Integer.
Компилятор не увидит ничего плохого. Мы получим ошибку уже ВО ВРЕМЯ ВЫПОЛНЕНИЯ (в Runtime).
Так же если не делать проверку (instanceof) на принадлежность к классу, то иногда можно получить ClassCastException –аварийное завершение программы.
С появлением Generics необходимость в проверке и явном приведении типа отпала:
2. Что такое дженерики? Какую проблему они решают?
Дженерики это грубо говоря набор инструкций для того, чтобы ты мог проверить свой код и не положить что-то туда, где его быть не должно. Позволяют отследить косяки на этапе компиляции, а при запуске программы эти инструкции удаляются, они не попадают в исполняющий байт код программы. Дженерики сделаны для удобства программиста.
Дженерики — это типы с параметром.
Одно из назначений — более сильная проверка типов во время компиляции и устранение необходимости явного приведения.
При создании дженерика ты указываешь не только его тип, но и тип данных, с которыми он должен работать. – List<String> myList1 = newArrayList<>();
Особенность такого списка заключается в том, что в него нельзя будет «запихивать» все подряд: он работает исключительно с объектами String.
Что можно параметризировать Дженериками?
- Параметризованные типы позволяют объявлять классы, интерфейсы, методы, конструкторы и поля, где тип данных, которыми они оперируют, указан в виде параметра. Используя дженерики, можно создать единственный класс, например, который будет автоматически работать с разными типами данных.
- Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются параметризованными или обобщениями, параметризованными (обобщёнными) классами или параметризованными (обобщёнными) методами.
Подробнее можно посмотреть тут!
Ограничения Generic?
Дженерики работают только с объектами! Нельзя использовать примитивные типы.
Нельзя создавать экземпляр по параметру типа. Ни обычный объект, ни массив.
Нельзя создавать обобщенные статические переменные и методы. Но объявить статические обобщенные методы со своими параметрами типа всё же можно: Невозможно создать массив параметра типа
Collection c;
T[] ta;
new T[10]; // Ошибка !! Невозможно создать массив Generic-классов
new ArrayList>();
List>[] la = new List>[10]; // Ошибка !!
GenericSub[] gens = new GenericSub[10]; // Ошибка !!
3. Какие типы дженериков бывают? (Generic Types и Raw Types)
В дженериках есть две категории: типизированные типы (Generic Types) и "сырые" типы (Raw Types).
При использовании классов-дженериков ни в коем случае не превращай generic type в raw type. Generic Types - Типизированные типы — с указанием "уточнения":
List myList1 = new ArrayList<>(); Generic Types — это класс-дженерик, с указанием типа.
Обеспечивает безопасность. В этот список нельзя будет «запихивать» все подряд: он работает исключительно с объектами String. Raw Types - Сырые типы — это типы без указания "уточнения" в фигурных скобках (angle brackets):
ListmyList1 = newArrayList<>(); Raw type — это класс-дженерик, из которого удалили его тип.
Если мы захотим обработать эту коллекцию (например, в ней лежат значения int и String) и вытащить из неё String, то нам придётся делать отдельные проверки, иначе всё упадёт с ошибкой.
Как следствие, мы теряем безопасность типов, если используем необработанный тип, такой как List , но не если вы используете параметризованный тип, такой как List
4. Хоть где-то можно и нужно использовать Raw Types в своём коде без риска получить ошибку?
К сожалению, дженерики Java не являются овеществленными, есть два исключения, когда необработанные типы (Raw Types) должны использоваться в новом коде:
- Литералы класса, например List.class, а не List.class.
- Операнд instanceof , например o instanceof Set, а не o instanceof Set.
5. Чем необработанный тип (Raw Types) отличается от использования > в качестве параметра типа?
List
6. Какой механизм обеспечивает обратную совместимость сырых типов и дженериков?
Стирание типов в runtime. Дженерики позволяют проверять типы во время компиляции. Во время исполнения программы информации о типах не доступна.
7. Понятие терминов Non-Reifiable Types и Reifiable Types?
- Reifiable-type — это тип, информация о котором полностью доступна во время выполнения.
В языке Java к ним относятся примитивы, raw-types, а также типы, не являющиеся дженериками.
- Non-ReifiableTypes — это типы, информация о которых стирается и становится недоступной во время выполнения. Это как раз дженерики — List, List и т.д.
(Отсюда и проблема со стиранием типа).
Так же создание массивов из Non-Reifiable Type в Java запрещено.
Подробнее можно прочитать тут!
8. Что такое стирание типа в дженериках? (Всё очень запутанно)
Cтирание типов относится к процессу загрузки, с помощью которого явные аннотации типов удаляются из программы перед её выполнением во время выполнения. Соответственно стирание типа происходит во время компиляции. Весь написанный тобой Java-код превратится в байт-код, в нём не будет информации о типах-параметрах.
Внутри байт-кода твой список List cats не будет отличаться от List strings. В байт-коде ничто не будет говорить о том, что cats — это список объектов Cat. Информация об этом сотрется во время компиляции, и в байт код попадет только информация о том, что у тебя в программе есть некий список List
5. Если поле типизировано дженериком как в байт коде будет представлен этот тип?
Box b = new Box();
String x = b.getDefault();
преобразуется в
Box b = new Box();
String x = (String) b.getDefault(); Стирание типов означает выбрасывание тегов типов, созданных во время разработки, или выведенных тегов типов во время компиляции таким образом, чтобы скомпилированная программа в двоичном коде не содержала никаких тегов типов. И это относится ко всем языкам программирования, компилируемым в двоичный код, за исключением некоторых случаев, когда вам нужны теги времени выполнения. Эти исключения включают, например, все экзистенциальные типы (Java ссылочные типы, которые являются подтипами, любой тип во многих языках, типы объединения). Причина стирания типов заключается в том, что программы преобразуются в язык, который в некотором роде является однотипным (двоичный язык допускает только биты), поскольку типы являются только абстракциями и утверждают структуру для своих значений и соответствующую семантику для их обработки.
Как можно обойти стирание типов?
В Java есть класс Class. Используя его, мы можем получить класс любого нашего объекта:
Class classInt = Integer.class;
Class classInt2 = Integer.class; //ошибка компиляции!
для объекта Integer.class является не просто Class, а Class. Типом объекта string.class является не просто Class, Class, и т.д. Мы просто передаём нужный класс-параметр в конструктор нашего класса-дженерика:
TestClass testString = new TestClass<>(MySecretClass.class);
Благодаря этому мы сохранили информацию о типе-параметре и уберегли её от стирания. В итоге мы смогли создать объект T! :)
9. Что такое даймонд оператор? Примеры использования? (<>)
Diamond <> применчяется только вместе с new
List myList1 = new ArrayList<>();
Компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа.
На самом деле, если в левой части указан дженерик, а справа не указан, компилятор сможет вывести тип:
List list = new ArrayList();
Однако это будет смешиванием нового стиля с дженериками и старого стиля без них. И это крайне нежелательно. При компиляции кода выше мы получим сообщение: Note: HelloWorld.java uses unchecked or unsafe operations. У ArrayList есть конструктор, который принимает на вход коллекцию.
И вот тут-то и кроется коварство. Без diamond <> синтаксиса компилятор не понимает, что его обманывают, а вот с diamond — понимает.
Поэтому, правило #1: всегда использовать diamond синтаксис, если мы используем типизированные типы. В противном случае мы рискуем пропустить, где у нас используется raw type.
3. Почему последняя строчка не скомпилируется?
List arrayLists = new ArrayList();
ArrayList arrayList = new ArrayList();
Потому что параметры типов у этих дженериков не совпадают!
Wildcard (>)
1. Что такое wildcard?
Для решения проблемы совместимости используется Wildcard («?»). Он не имеет ограничения в использовании (то есть имеет соответствие с любым типом) и в этом его плюсы.
Благодаря Wildcard > можно сделать универсальный метод (или переменную), работающий с разными типами данных. List super Integer> ints = nums;
Запись вида extends T> или super T> — называется wildcard или символом подстановки, с верхней границей (extends) или с нижней границей (super).
Или просто >. Принцип PECS.
Принцип PECS - Producer Extends Consumer Super
1. Принцип PECS, что это? (нужен для проектировки удобного API)
Если метод имеет аргументы с параметризованным типом,то в случае, если аргумент - производитель (producer), нужно использовать ? extends T, а если аргумент - потребитель (consumer), нужно использовать ? super T.
Производитель и потребитель, кто это такие? Очень просто: если метод читает данные из аргумента, то этот аргумент - производитель, а если метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что, определяя производителя или потребителя, мы рассматриваем только данные типа T.
Пример:Map getAll(Collection extends T> keys); В случае, если аргумент является и потребителем, и производителем одновременно - например, если метод одновременно и читает из коллекции, и пишет в неё (плохой стиль, но всякое бывает) - тогда его нужно оставить как есть >.
С возвращаемыми значениями тоже ничего делать не нужно - никакого удобства использование wildcard-ов в этом случае пользователю не принесёт, а лишь вынудит его использовать wildcard-ы в собственном коде. Другие примеры потребителя и производителя
Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:
interface Factory {
T create(Object... args); // Создаёт новый экземпляр объекта заданного типа и возвращает его.
}
Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:
interface Cloner {
T clone(T obj); // Клонирует исходный obj и возвращает копию
}
Между объектом и коллекцией объектов есть важное различие, какое?
String str = new String("Test!");
Object obj = str; // никаких проблем List strings = new ArrayList();
List // ошибка компиляции!
Если класс B является наследником класса А, то Collection при этом — не наследник Collection.