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());
}Результат:
3StringBuilder::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().