Вступление

Привет!

Указание универсального типа позволяет Java выполнять проверку типа во время компиляции. Но при использовании универсальных типов в вашем коде из-за «стирания типа», которое происходит во время компиляции, параметры универсального типа преобразуются в тип Object. Это делает общие параметры типа неспособными вызывать другие методы, кроме Object.

Что, если мы хотим вызвать методы, отличные от тех, что в классе Object? В этой статье объясняется, что такое «подстановочные знаки», «границы» и «стирание типа». И как использовать подстановочные знаки для повышения гибкости при использовании дженериков.

Тип Стирание

Википедия определяет стирание типа как:

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

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

class Container<T> {
    private T contents;
public Container(T contents) {
        this.contents = contents;
    }
public T getContents() {
        return contents;
    }
public static void main(String[] args) {
        Container<String> container = new Container<>("Hello!");
        String contents = container.getContents();
        System.out.println(contents);// Hello!
    }
}

Вы можете подумать, набирая Container<String> container = new Container<>(“Hello!”);, что заменяете тип T на String. Но это не тот случай, во время компиляции компилятор Java удаляет параметр универсального типа T из кода и заменяет его типом Object:

class Container {
    private Object contents;
public Container(Object contents) {
        this.contents = contents;
    }
public Object getContents() {
        return contents;
    }
public static void main(String[] args) {
        Container container = new Container("Hello!");
        String contents = (String) container.getContents();
        System.out.println(contents);
    }
}

Обратите внимание, как String contents = container.getContents(); преобразуется в String contents = (String) container.getContents();. Как вы можете видеть, компилятор вставил приведение при вызове метода getContents().

Универсальный тип подстановочного знака

Подстановочный знак, представленный ?, полезен, когда вы хотите определить «любой тип».

Неограниченный подстановочный знак

Одно из общих ограничений - вы не можете, например, передать аргумент List ‹Integer› параметру List ‹Object›; это приведет к ошибке времени компиляции. На самом деле это хорошо, потому что Java защищает вас от вас!

public static void processList(List<Object> list) {
    for (Object o : list)
        System.out.print(o);
}
public static void main(String[] args) {
    List<Integer> numbers = List.of(1, 2);
    processList(numbers); // compile-time error
}

Этот пример не работает, потому что List<Integer> не является подтипом List<Object>. Как и следовало ожидать, тип List<Object> в параметре processList необходимо заменить на List<?> (читайте: список неизвестного типа)

Теперь я могу передать любой вид List в processList метод:

public static void processList(List<?> list) {
    for (Object o : list)
        System.out.print(o);
}
public static void main(String[] args) {
    List<Integer> list = List.of(1, 2);
    processList(list);// 12
    System.out.println();
    List<String> strings = List.of("Hello", "World!");
    processList(strings);// HelloWorld!
}

ограниченный подстановочный знак

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

public static void transformList(List<? extends CharSequence> list){
    List<Integer> charsCount = list.stream()
            .map(CharSequence::length)
            .collect(Collectors.toList());
    System.out.println(charsCount);
}
public static void main(String[] args) {
    List<String> stringList = List.of("Hello", "World", "!");
    transformList(stringList);// [5, 5, 1]
    List<StringBuilder> stringBuilders = List.of(new StringBuilder("Java"), new StringBuilder("Rocks"));
    transformList(stringBuilders);// [4, 5]
}

Что делает transformList, так это принимает List и преобразует его в List<Integer>, выражая длину каждого элемента.

Обратите внимание, что параметр transformList равен List<? extends CharSequence>, что означает, что этот метод будет принимать любой список с верхним ограничением до CharSequence, например List<String>, List<StringBuilder>, любой список с подтипом CharSequence может быть передан в качестве аргумента этому методу.

Заворачивать

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

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