Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

PECS и стирание типов

В уроке об ограничениях типов появились подстановочные знаки ? extends и ? super. Здесь разберём правило, по которому их выбирают, а также стирание типов — особенность Java, из-за которой обобщённые типы во время работы программы ведут себя не так, как можно ожидать.

Правило PECS

Запомнить, какой знак ставить, помогает сокращение PECS: Producer Extends, Consumer Super. Если коллекция производит значения для метода (он из неё читает), пишите ? extends T. Если она потребляет значения (метод в неё пишет) — ? super T:

<T> void copy(List<? extends T> source, List<? super T> target) {
    for (T item : source) {
        target.add(item);
    }
}
 
void main() {
    List<Integer> ints = List.of(1, 2, 3);
    List<Number> numbers = new ArrayList<>();
    copy(ints, numbers);
    IO.println(numbers);
}

Результат:

[1, 2, 3]

Благодаря подстановочным знакам метод копирует целые числа в список Number или даже Object. Так устроен стандартный Collections.copy().

Знак ? без ограничений

List<?> означает «список чего угодно». Подходит, когда методу безразличен тип элементов: элементы читаются как Object, а добавить в такой список ничего нельзя:

void printAll(List<?> items) {
    for (Object item : items) {
        IO.print(item + " ");
    }
    IO.println("(" + items.size() + ")");
}
 
void main() {
    printAll(List.of("a", "b"));
    printAll(List.of(1, 2, 3));
}

Результат:

a b (2)
1 2 3 (3)

Стирание типов

Параметры типа существуют только для компилятора. Проверив типы, он стирает их: в готовой программе List<String> и List<Integer> — один и тот же класс ArrayList:

void main() {
    List<String> words = new ArrayList<>();
    List<Integer> numbers = new ArrayList<>();
    IO.println(words.getClass() == numbers.getClass());
    IO.println(words.getClass().getName());
}

Результат:

true
java.util.ArrayList

Так обобщённые типы остались совместимы со старым кодом, написанным до Java 5. Но у этого есть последствия.

Нельзя создать new T()

Во время работы программы неизвестно, что такое T, поэтому new T() не скомпилируется. Решение — передать в метод «фабрику» объектов, Supplier<T>:

<T> List<T> fill(Supplier<T> factory, int count) {
    List<T> result = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        result.add(factory.get());
    }
    return result;
}
 
void main() {
    List<StringBuilder> buffers = fill(StringBuilder::new, 3);
    IO.println(buffers.size());
}

Результат:

3

StringBuilder::new — ссылка на конструктор, она создаёт новый объект при каждом вызове get(). По той же причине нельзя создать массив new T[10].

instanceof и обобщённые типы

Проверить, что объект — именно List<String>, невозможно: информация о String стёрта. Компилятор разрешает только проверку без параметра — List<?>:

void main() {
    Object value = List.of("a", "b");
    if (value instanceof List<?> list) {
        IO.println("Список из " + list.size() + " элементов");
    }
}

Результат:

Список из 2 элементов

Запись value instanceof List<String> для переменной типа Object вызовет ошибку компиляции Object cannot be safely cast to List<String>.

Частая ошибка

Из-за стирания два метода, которые отличаются только параметром типа, после компиляции выглядят одинаково. Такую перегрузку Java не пропустит:

void print(List<String> words) {
    IO.println("строки");
}
 
void print(List<Integer> numbers) {
    IO.println("числа");
}
 
void main() {
}

Результат:

Main.java:5: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
void print(List<Integer> numbers) {
     ^
1 error
error: compilation failed

Дайте методам разные имена, например printWords() и printNumbers().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.