Что такое дженерики?

Недавно мне пришлось объяснять, что такое дженерики и как они используются в языке Java. В двух словах. Через пару минут.

Я закончил тем, что заявил, что дженерики - это механизм, который позволяет нам писать код, который не заботится о типе объектов, которые он обрабатывает, и в то же время предоставляет компилятору достаточно информации для сохранения безопасности типов. Небольшое примечание для компилятора.

Мы не можем использовать Object для постоянного представления любого типа

Давайте начнем с классического примера списка. Забыв на мгновение, что List и LinkedList являются универсальными типами, следующее является допустимым Java и создает список, содержащий любой тип объекта.

List myList = new LinkedList();

Это означает, что это законно:

myList.add(“Hello”);

И это тоже законно:

myList.add(1);

Эта смесь типов кажется подозрительной. Можем ли мы также написать следующий цикл?

for (Integer i : myList) System.out.println(i);

Нет. Поскольку список не содержит информации о типах своих элементов, компилятор не может проверить, допустимо ли каждое присвоение переменной i. Вместо этого мы должны написать:

for (Object i : myList) System.out.println(i);

Если мы намерены обрабатывать каждый элемент списка как целое число для выполнения некоторой арифметической операции, это становится некрасивым:

for (Object i : myList) System.out.println(((Integer) i) * 2);

Фактически, это стало настолько уродливым, что любой элемент, не являющийся целым числом, теперь приведет к сбою приложения с ClassCastException. Это проблема при использовании Object для представления некоторого неизвестного типа: код становится хрупким, когда делается предположение о том, что представляет этот тип.

Устранение проблемы с помощью параметров типа

Когда мы воспользуемся преимуществом универсального типа и напишем вместо него следующее, эта проблема будет решена, потому что теперь компилятор может выполнять все проверки за нас:

List<Integer> myList = new LinkedList<Integer>()
myList.add(1);
myList.add(“Hello”); // compiler error: now it is illegal.
for (Integer i : myList) System.out.println(i * 2); // beautiful.

Теперь мы можем быть уверены, что в списке нет ничего, кроме целых чисел.

Параметр типа - это, прежде всего, инструкция для компилятора. Сограждане идут на втором месте.

interface SomeCollection<T> {
  void add(T element);
}

Когда мы объявляем интерфейс, подобный приведенному выше, мы сообщаем компилятору, что он должен гарантировать, что аргумент метода add () всегда будет иметь тип, указанный в пользовательском коде (T). Какой бы тип он ни был, нам все равно, пока он всегда один и тот же!

Общие типы также могут использоваться на уровне метода. Допустим, мы объявляем эту функцию в каком-то классе:

public <T,R> Optional<R> process(T input, Predicate<T> validator, Function<T,R> processor) {
  return validator.test(input)
    ? Optional.ofNullable(processor.apply(input));
    : Optional.empty();
}

Эта функция немного запутана, но тем не менее является подходящим примером. Здесь мы используем универсальные типы, чтобы сообщить компилятору две вещи:

  1. Что валидатор и процессор должны обрабатывать совместимые типы, представленные T.
  2. Что наша функция возвращает экземпляр Optional, обертывающий возвращаемое значение процессора, представленное R.

Опять же, нам все равно, какие это типы. Мы только хотим убедиться, что они совместимы.

Когда универсальный тип должен иметь некоторую стандартную семантику

Можно наложить ограничения на объявления универсального типа с помощью ключевого слова extends, чтобы раскрыть известный интерфейс. Таким образом, универсальный тип становится значимым и может использоваться внутри метода или класса. Однако основная концепция остается. Вместо универсального типа можно использовать любой тип, если он реализует указанный интерфейс.

class MyWrapper<T extends MyOtherInterface> {
  // use T in some manner
}

Заключение

Универсальные типы в Java позволяют нам писать высокоуровневый код, который может обрабатывать различные типы объектов, не зная, какие именно типы являются этими типами, сохраняя при этом безопасность типов.