Основы Java Generics

1. Вступление

Java Generics были введены в JDK 5.0 с целью уменьшения ошибок и добавления дополнительного уровня абстракции над типами.

Эта статья представляет собой краткое введение в Generics in Java, цель, стоящую за ними, и как их можно использовать для улучшения качества нашего кода.

2. Потребность в дженериках

Давайте представим сценарий, в котором мы хотим создать список на Java для хранения Integer ; мы можем испытать желание написать:

List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();

Удивительно, но компилятор пожалуется на последнюю строку. Он не знает, какой тип данных возвращается. Компилятору потребуется явное приведение:

Integer i = (Integer) list.iterator.next();

Нет контракта, который мог бы гарантировать, что тип возвращаемого списка - Integer. Определенный список может содержать любой объект. Мы только знаем, что мы получаем список, проверяя контекст. При просмотре типов он может гарантировать только то, что это Object , поэтому требуется явное приведение, чтобы гарантировать безопасность типа.

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

Было бы намного проще, если бы программисты могли выразить свое намерение использовать определенные типы, а компилятор мог бы гарантировать правильность такого типа. Это основная идея дженериков.

Давайте изменим первую строку предыдущего фрагмента кода:

List<Integer> list = new LinkedList<>();

Добавляя оператор diamond <>, содержащий тип, мы сужаем специализацию этого списка только до Integer типа, т.е. мы указываем тип, который будет храниться в списке. Компилятор может применять тип во время компиляции.

В небольших программах это может показаться тривиальным дополнением, однако в больших программах это может значительно повысить надежность и облегчить чтение программы.

3. Общие методы

Универсальные методы - это те методы, которые написаны с одним объявлением метода и могут вызываться с аргументами разных типов. Компилятор обеспечит правильность того, какой тип используется. Вот некоторые свойства универсальных методов:

  • Универсальные методы имеют параметр типа (оператор Diamond включен

тип) перед возвращаемым типом объявления метода ** Параметры типа могут быть ограничены (границы объяснены позже в

статья) ** Общие методы могут иметь различные параметры типа, разделенные запятыми

в сигнатуре метода ** Тело метода для универсального метода подобно обычному методу

Пример определения универсального метода для преобразования массива в список:

public <T> List<T> fromArrayToList(T[]a) {
    return Arrays.stream(a).collect(Collectors.toList());
}

В предыдущем примере <T> в ​​сигнатуре метода подразумевает, что метод будет иметь дело с универсальным типом T . Это необходимо, даже если метод возвращает void.

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

public static <T, G> List<G> fromArrayToList(T[]a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

Мы передаем функцию, которая преобразует массив с элементами типа T в список с элементами типа G. Примером может быть преобразование Integer в его String представление:

@Test
public void givenArrayOfIntegers__thanListOfStringReturnedOK() {
    Integer[]intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);

    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

Стоит отметить, что рекомендация Oracle состоит в том, чтобы использовать заглавную букву для представления универсального типа и выбрать более описательную букву для представления формальных типов, например, в Java Collections T используется для типа, K для ключа, V для значения.

3.1. Ограниченные Обобщения

Как упоминалось ранее, параметры типа могут быть ограничены. Ограниченный означает « restricted », мы можем ограничить типы, которые могут быть приняты методом.

Например, мы можем указать, что метод принимает тип и все его подклассы (верхняя граница) или тип все его суперклассы (нижняя граница).

Чтобы объявить ограниченный сверху тип, мы используем ключевое слово extends после типа, за которым следует верхняя граница, которую мы хотим использовать. Например:

public <T extends Number> List<T> fromArrayToList(T[]a) {
    ...
}

Ключевое слово extends используется здесь для обозначения того, что тип T расширяет верхнюю границу в случае класса или реализует верхнюю границу в случае интерфейса. Тип также может иметь несколько верхних границ следующим образом:

<T extends Number & Comparable>

если один из типов, которые расширены T , является классом (т.е. Number ), он должен быть помещен первым в список границ, иначе это вызовет ошибку времени компиляции.

4. Использование подстановочных знаков с обобщениями

Подстановочные знаки представлены в Java знаком вопроса « ? » И используются для обозначения неизвестного типа. Подстановочные знаки особенно полезны при использовании универсальных шаблонов и могут использоваться в качестве типа параметра, но, во-первых, следует обратить внимание на важное примечание.

  • Известно, что Object является супертипом всех классов Java, однако коллекция Object не является супертипом какой-либо коллекции. **

Например, List <Object> не является супертипом List <String> , а присвоение переменной типа List <Object> переменной типа List <String> приведет к ошибке компилятора. Это сделано для предотвращения возможных конфликтов, которые могут возникнуть, если мы добавим гетерогенные типы в одну коллекцию.

То же правило применяется к любой коллекции типа и его подтипов.

Рассмотрим этот пример:

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

если мы представляем подтип Building , например, House , мы не сможем использовать этот метод со списком House , даже если House является подтипом Building . Если нам нужно использовать этот метод с типом Building и всеми его подтипами, то ограниченный подстановочный знак может творить чудеса:

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

Теперь этот метод будет работать с типом Building и всеми его подтипами.

Это называется верхним ограниченным подстановочным знаком, где тип Building - верхняя граница

Подстановочные знаки также могут быть указаны с нижней границей, где неизвестный тип должен быть супертипом указанного типа. Нижние границы можно указать с помощью ключевого слова super , за которым следует определенный тип, например, <? super T> означает неизвестный тип, который является суперклассом T (= T и всех его родителей).

5. Тип Erasure

Дженерики были добавлены в Java для обеспечения безопасности типов и того, что дженерики не будут вызывать перегрузок во время выполнения, компилятор применяет процесс под названием type erasure к дженерикам во время компиляции.

Стирание типа удаляет все параметры типа и заменяет их на их границы или на Object , если параметр типа не ограничен. Таким образом, байт-код после компиляции содержит только обычные классы, интерфейсы и методы, что гарантирует отсутствие новых типов. Правильное приведение применяется также к типу Object во время компиляции.

Это пример стирания типа:

public <T> List<T> genericMethod(T t) {
    return list.stream().collect(Collectors.toList());
}

При компиляции неограниченный тип T заменяется на Object следующим образом:

public List<Object> fromArrayToList(Object a) {
    return list.stream().collect(Collectors.toList());
}

если тип ограничен, то тип будет заменен границей во время компиляции следующим образом:

public <T extends Building> void genericMethod(T t) {
    ...
}

изменится после компиляции:

public void genericMethod(Building t) {
    ...
}

6. Заключение

Обобщения Java - мощное дополнение к языку Java, поскольку оно облегчает работу программиста и снижает вероятность ошибок. Обобщения обеспечивают правильность типов во время компиляции и, что самое важное, позволяют реализовывать универсальные алгоритмы, не вызывая дополнительных затрат для наших приложений.

Исходный код, который сопровождает статью, доступен на GitHub over .