Дженерики или обобщения в Java - Вопросы
Всего: 8 вопросов
1. Что такое дженерики в Java и зачем они нужны?
Что такое дженерики в Java и зачем они нужны?
Дженерики (generics, обобщения) - механизм параметризации типов, появившийся в Java 5: класс, интерфейс или метод объявляется с параметром типа (T), а конкретный тип подставляется при использовании - List<String>, Matrix<Integer>.
Главная цель - безопасность типов. Без дженериков коллекция хранит Object, элементы приводятся вручную, и ошибка проявляется только при выполнении:
List rawList = new ArrayList(); // сырой тип (raw type)
rawList.add("42");
Integer n = (Integer) rawList.get(0); // компилируется, но ClassCastException
List<Integer> numbers = new ArrayList<>();
// numbers.add("42"); // ошибка компиляции
numbers.add(42);
Integer m = numbers.get(0); // приведение не нужно
С дженериками компилятор не пропускает неподходящие значения и сам вставляет приведения. Тип без аргументов (List вместо List<Integer>) называется сырым типом и оставлен только для совместимости со старым кодом.
2. Что означает T в Java и какие имена параметров типа приняты?
Что означает T в Java и какие имена параметров типа приняты?
T - имя параметра типа, заполнитель, вместо которого подставляется конкретный ссылочный тип. В class Matrix<T> буква T - параметр типа, а в Matrix<Integer> тип Integer - аргумент типа.
По соглашению параметр называют одной заглавной буквой: T - любой тип, E - элемент коллекции (List<E>), K/V - ключ и значение (Map<K, V>), N - число, R - результат (Function<T, R>), S, U - второй и третий тип.
Параметров может быть несколько, через запятую:
class TwoGen<T, V> { ... }
TwoGen<Integer, String> twoGen = new TwoGen<>(88, "Generics");
Аргументом типа может быть только ссылочный тип: Matrix<int> - ошибка компиляции, нужен Matrix<Integer>. С Java 7 справа можно писать diamond-оператор <> - компилятор выведет аргументы из левой части.
3. Что такое ограниченный тип (bounded type) в дженериках Java?
Что такое ограниченный тип (bounded type) в дженериках Java?
Ограниченный тип - параметр типа с верхней границей: <T extends Number> разрешает подставлять только Number и его подклассы. Взамен внутри класса можно вызывать методы границы:
class Average<T extends Number> {
double sum(T[] array) {
double s = 0;
for (T v : array) s += v.doubleValue(); // метод Number
return s;
}
}
// Average<String> - ошибка: type argument String is not within bounds
Граница-интерфейс тоже задаётся словом extends, а не implements: <T extends Serializable>. Несколько границ объединяют знаком &, причём класс указывается первым: <T extends MyClass & MyInterface1 & MyInterface2> - аргументом может быть только подкласс MyClass, реализующий оба интерфейса.
4. Что такое wildcard ? в Java и чем ? extends отличается от ? super?
Что такое wildcard ? в Java и чем ? extends отличается от ? super?
Wildcard (метасимвольный аргумент) ? обозначает неизвестный аргумент типа. Он нужен потому, что параметризованные типы инвариантны: List<Integer> не является подтипом List<Number>, хотя Integer - подкласс Number.
List<?>- список с любым аргументом; элементы читаются какObject, добавлять нельзя (кромеnull).List<? extends Number>-Numberили подкласс; читать можно какNumber, добавлять нельзя.List<? super Integer>-Integerили суперкласс; добавлятьIntegerможно, читать - только какObject.
Правило PECS (Producer Extends, Consumer Super): параметр, который отдаёт элементы, объявляют через ? extends T, а который принимает - через ? super T:
static <T> void copy(List<? super T> dest, List<? extends T> src)
5. Как объявить обобщённый метод в Java и как компилятор определяет его аргумент типа?
Как объявить обобщённый метод в Java и как компилятор определяет его аргумент типа?
Обобщённый метод объявляет собственные параметры типа в угловых скобках перед типом возвращаемого значения. Его можно объявить и в обычном, необобщённом классе, в том числе как static:
public static <T> boolean isIn(T x, T[] array) { ... }
isIn(2, intArray); // T выведен как Integer
GenMethodExample.<Integer>isIn(2, intArray); // аргумент типа указан явно
Обычно аргумент типа компилятор выводит сам по аргументам вызова. Подвох: isIn("2", intArray) скомпилируется и вернёт false - T будет выведен как общий супертип String и Integer. С явным <Integer> это станет ошибкой компиляции.
Обобщёнными могут быть и конструкторы, даже в необобщённом классе: public <T extends Number> GenConstructor(T arg).
6. Как унаследовать обобщённый класс или реализовать обобщённый интерфейс?
Как унаследовать обобщённый класс или реализовать обобщённый интерфейс?
Подкласс (или класс, реализующий интерфейс) обязан передать вверх все аргументы типа - своим параметром или конкретным типом:
class GenericSub<T> extends GenericSuper<T> { ... } // передаёт свой T
class IntegerHolder extends GenericSuper<Integer> { ... } // фиксирует тип
class LabeledHolder<T, V> extends GenericSuper<T> { ... } // добавляет свой параметр
class Echo<T> implements MyInterface<T> { ... }
class Upper implements MyInterface<String> { ... }
Запись class Echo implements MyInterface<T> без объявления T у самого класса не скомпилируется: cannot find symbol: class T. Суперклассом обобщённого класса может быть и обычный необобщённый класс. Так устроены и интерфейсы JDK: Comparable<T>, Comparator<T>, List<E>.
7. Что такое стирание типов (type erasure) в Java?
Что такое стирание типов (type erasure) в Java?
Стирание типов - способ реализации дженериков: компилятор проверяет аргументы типа, а затем удаляет их из байт-кода. T заменяется своей границей (Object или, например, Number для <T extends Number>), а в местах использования вставляются приведения.
Поэтому во время выполнения List<String> и List<Integer> - один класс:
System.out.println(new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass()); // true
Из-за стирания нельзя написать new T() - тип передают явно объектом Class<T> (type token): type.getDeclaredConstructor().newInstance().
Стираются аргументы у объектов, но не в объявлениях: для class IntegerHolder extends GenericSuper<Integer> метод getGenericSuperclass() вернёт GenericSuper<java.lang.Integer>. На этом построен приём super type token (TypeReference в Jackson).
8. Какие ограничения есть у дженериков в Java?
Какие ограничения есть у дженериков в Java?
Почти все ограничения - следствие стирания типов. Нельзя:
- использовать примитив как аргумент типа:
Matrix<int>- нуженMatrix<Integer>; - создать объект или массив по параметру:
new T(),new T[10](generic array creation); - создать массив параметризованного типа:
new GenericSub<Integer>[10]- допустимоnew GenericSub<?>[10]; - использовать
Tкласса в статическом поле или методе:static T ob;(но статический метод со своим параметромstatic <V> void m(V v)- можно); - перегрузить методы, различающиеся только аргументом типа:
print(List<String>)иprint(List<Integer>)-name clash; - объявить обобщённый класс исключения:
class MyEx<T> extends Exception.
instanceof GenericSub<?> работает всегда. С Java 16 разрешён и конкретный аргумент, если приведение проверяемое: для GenericSuper<Integer> x проверка x instanceof GenericSub<Integer> компилируется, а для Object o проверка o instanceof GenericSub<Integer> - ошибка компиляции.