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 означает полностью независимые объекты?
Нет. Независим набор ссылок в списке, но сами объекты не копируются рекурсивно. Для глубокой независимости нужна отдельная модель данных или явное копирование вложенных изменяемых значений.