ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Generics и коллекции Java: extends, super, ключи и неизменяемость

Автор: · Обновлено

Generics позволяют компилятору проверить, какие элементы проходят через коллекцию. Но параметр List<String> не отвечает на вопросы о допустимости null, возможности изменения и…

Generics позволяют компилятору проверить, какие элементы проходят через коллекцию. Но параметр List<String> не отвечает на вопросы о допустимости null, возможности изменения и смысле равенства. Эти контракты нужно согласовывать отдельно.

Почему List<Integer> не является List<Number>

Integer является подтипом Number, однако из этого не следует такое же отношение между списками. Если бы список Integer разрешалось использовать как изменяемый List<Number>, в него можно было бы добавить Double. Читатель исходного списка затем получил бы значение неверного типа. Поэтому обычные generic-типы инвариантны. Параметры типов и наследование.

Не исправляйте запрет приведением к сырому List: raw type убирает полезную проверку и переносит ошибку на выполнение. Лучше описать действие метода: читает ли он значения, записывает ли их или делает оба действия.

Источник extends, получатель super

Для чтения значений как Number подходит Collection<? extends Number>. В неё нельзя безопасно добавить произвольный Number: реальным типом может быть Integer. Для записи Integer подходит Collection<? super Integer>; получателем может быть коллекция Integer, Number или Object. Учитывайте, что wildcard не означает неизменяемость контейнера: например, операция очистки может оставаться допустимой. Wildcards на dev.java.

Полная программа GenericContracts.java использует один параметр T, связывающий источник и получателя:

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GenericContracts {
    record UserKey(String login) {}

    static <T> void copyInto(Collection<? extends T> source,
                             Collection<? super T> target) {
        for (T element : source) {
            target.add(element);
        }
    }

    public static void main(String[] args) {
        List<Integer> source = List.of(2, 4, 6);
        List<Number> target = new ArrayList<>();
        copyInto(source, target);
        target.add(1.5);
        System.out.println(target);

        Map<UserKey, String> roles = new HashMap<>();
        roles.put(new UserKey("mira"), "reader");
        System.out.println(roles.get(new UserKey("mira")));

        List<String> editable = new ArrayList<>(List.of("draft"));
        List<String> snapshot = List.copyOf(editable);
        editable.add("published");
        System.out.println(editable);
        System.out.println(snapshot);
    }
}

Соберите javac -encoding UTF-8 --release 25 GenericContracts.java, запустите java GenericContracts. Результат:

[2, 4, 6, 1.5]
reader
[draft, published]
[draft]

<T> перед void объявляет параметр именно метода. В нашем вызове компилятор связывает Integer-источник с совместимым Number-получателем. new ArrayList<>() здесь выбран явно: сигнатура сама по себе не гарантирует, что переданный target поддерживает add.

Ключи Map должны сохранять смысл равенства

record UserKey — краткое описание носителя данных. Для его компонентов компилятор создаёт доступ и согласованные методы равенства; два экземпляра с одним логином подходят для поиска одной записи. Компонент record нельзя переназначить, но вложенный изменяемый объект сам по себе не замораживается. Поэтому в примере ключ содержит неизменяемый String. Records.

Если изменить поле обычного ключа, участвующее в equals и hashCode, после помещения в HashMap, последующий поиск может не найти запись там, где она была размещена. Нельзя лечить такую ошибку дополнительными проверками null: нужно восстановить стабильность ключа. Аналогично для TreeMap сравнение должно оставаться согласованным с использованием ключей. Выбор неизменяемого ключа.

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

List.copyOf создаёт немодифицируемый список с текущими элементами; добавление в исходный ArrayList не меняет набор элементов полученного списка. Но ссылки на сами элементы остаются ссылками: копирования каждого вложенного объекта здесь нет. Collections.unmodifiableList создаёт защищённое от прямой записи представление; изменения исходного списка через другую ссылку могут быть видны в нём. List.copyOf), Collections.unmodifiableList).

В проекте полезно явно выбрать момент владения: метод получает живую рабочую коллекцию или фиксирует её состав для дальнейшей обработки. Сигнатура List<T> не расскажет об этом без документации и понятного создания результата.

Задание на границы типов

Добавьте ещё один получатель List<Object> и скопируйте туда тот же Integer-источник. Затем отдельно, в учебной копии файла, попробуйте скопировать List<Double> в List<Integer>: компилятор должен отвергнуть вызов. Удалите ошибочный вызов перед обычным запуском.

Дополнительно передайте в copyInto немодифицируемый target и предскажите класс исключения. Это проверяет другой слой контракта: тип элементов подходит, но операция записи не поддерживается. Для рабочего API сформулируйте это требование в описании метода, а не маскируйте его под ошибку generic-синтаксиса.

Частые вопросы

Почему из Collection<? super Integer> читают Object?

Фактический получатель может быть Collection<Object> и уже содержать строки или другие объекты. Запись Integer безопасна, но обещать Integer для каждого ранее существовавшего элемента компилятор не может.

List.copyOf означает полностью независимые объекты?

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

Источники