Дженерики или обобщения в Java - Вопросы

Всего: 8 вопросов

1. 

Что такое дженерики в 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 - имя параметра типа, заполнитель, вместо которого подставляется конкретный ссылочный тип. В 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?

Ограниченный тип - параметр типа с верхней границей: <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 (метасимвольный аргумент) ? обозначает неизвестный аргумент типа. Он нужен потому, что параметризованные типы инвариантны: 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 и как компилятор определяет его аргумент типа?

Обобщённый метод объявляет собственные параметры типа в угловых скобках перед типом возвращаемого значения. Его можно объявить и в обычном, необобщённом классе, в том числе как 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?

Стирание типов - способ реализации дженериков: компилятор проверяет аргументы типа, а затем удаляет их из байт-кода. 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?

Почти все ограничения - следствие стирания типов. Нельзя:

  • использовать примитив как аргумент типа: 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> - ошибка компиляции.

Страница 1 из 1