Является ли List<Собака> подклассом List<Животное>? Почему дженерики в Java не являются неявно полиморфными?
Я немного запутался в том, как дженерики в Java обрабатывают наследование и полиморфизм.
Предположим, у нас есть следующая иерархия:
Животное (родитель)
Собака - Кошка (дочерние классы)
Итак, у меня есть метод doSomething(List<Animal> animals)
. Согласно всем правилам наследования и полиморфизма, я бы предположил, что List<Dog>
является List<Animal>
, а List<Cat>
тоже является List<Animal>
, и поэтому любой из этих списков можно передать в этот метод. Но не тут-то было. Если я хочу достичь такого поведения, мне нужно явно указать методу принимать список любых подклассов Animal, написав doSomething(List<? extends Animal> animals)
.
Я понимаю, что это поведение Java. Мой вопрос в том, почему? Почему полиморфизм обычно является неявным, но когда дело доходит до дженериков, его нужно указывать явно?
5 ответ(ов)
Нет, List<Dog>
не является List<Animal>
. Подумайте, что вы можете делать с List<Animal>
— вы можете добавить туда любое животное, включая кота. Теперь, можете ли вы логически добавить кота в помет щенков? Абсолютно нет.
// Неправильный код - иначе жизнь станет плохой
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList реализует List
List<Animal> animals = dogs; // Авуга авага
animals.add(new Cat()); // Ой!
Dog dog = dogs.get(0); // Это должно быть безопасно, верно?
В результате у вас оказывается оченьConfused cat.
Теперь вы не можете добавить Cat
в List<? extends Animal>
, потому что вы не знаете, это List<Cat>
. Вы можете извлечь значение и быть уверенным, что это будет Animal
, но не можете добавлять произвольные животные. Обратное верно для List<? super Animal>
— в этом случае вы можете безопасно добавить Animal
, но не знаете ничего о том, что может быть извлечено из него, поскольку это может быть List<Object>
.
Другие пользователи довольно хорошо объяснили, почему нельзя просто привести список потомков к списку суперкласса.
Тем не менее, многие ищут решение этой проблемы.
Так что решение данной задачи с версии Java 10 выглядит следующим образом:
List<S> supers = List.copyOf(descendants);
Эта функция выполнит приведение, если оно совершенно безопасно, или создаст копию, если приведение будет небезопасным. В результате получится неизменяемый список.
Для более глубокого объяснения (которое учитывает потенциальные подводные камни, упомянутые в других ответах) смотрите связанный вопрос и мой ответ на него за 2022 год: https://stackoverflow.com/a/72195980/773113.
Я бы сказал, что основная цель дженериков в том, что они не позволяют этого делать. Рассмотрим ситуацию с массивами, которые допускают такую типовую ковариантность:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
Этот код компилируется без ошибок, но вызывает ошибку во время выполнения (java.lang.ArrayStoreException: java.lang.Boolean
на второй строке). Это не типобезопасно. Цель дженериков — добавить безопасность типов на этапе компиляции, иначе можно было бы обойтись обычным классом без дженериков.
Некоторые ситуации требуют большей гибкости, и для этого предназначены ? super Class
и ? extends Class
. Первый используется, когда нужно вставить элемент в определенный тип Collection
, а второй — когда нужно безопасно читать из него. Однако единственный способ сделать и то, и другое одновременно — иметь конкретный тип.
Ваш вопрос касается использования дженериков в Java и ограничения типов для Consumer
и Supplier
. Давайте разберем это подробнее.
Первоначальный пример:
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
На первый взгляд, для передачи Consumer
и Supplier
с типом Animal
всё выглядит нормально. Однако это приводит к проблемам, если ваш Consumer
ожидает более конкретный тип, например, Mammal
, а Supplier
предоставляет объект Duck
. Хоть оба типа и являются подтипами Animal
, тут возникает несоответствие.
Чтобы избежать этих проблем, вы можете ввести ограничения между типами:
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
В этом варианте мы гарантируем, что Supplier
предоставляет объект подходящего типа для Consumer
.
С другой стороны, существует возможность определения типов по-другому:
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
Здесь мы определяем тип Supplier
, но допускаем, что Consumer
может обрабатывать более общий тип.
Также можно комбинировать эти подходы:
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
Такой подход позволяет использовать Consumer
, который принимает более общий тип Animal
, а Supplier
предоставляет более специфический объект. Например, вы можете передать Mammal
в Consumer
, который работает с Life
, но не сможете передать String
, так как это не является подтипом Life
.
Так что использование дженериков предоставляет гибкость, позволяя нам контролировать, какие типы могут быть переданы, и избегать потенциальных ошибок времени выполнения.
Логика такого поведения заключается в том, что Generics
следуют механизму стирания типов. Таким образом, на этапе выполнения вы не имеете возможности определить тип collection
, в отличие от массивов, где такой процесс стирания отсутствует. Возвращаясь к вашему вопросу...
Предположим, есть метод, как показано ниже:
add(List<Animal>){
//Вы можете добавить List<Dog или List<Cat>, и это скомпилируется в соответствии с правилами полиморфизма
}
Теперь, если Java позволяет вызывающему коду добавлять List типа Animal в этот метод, то вы можете добавить неправильный элемент в коллекцию, и на этапе выполнения это код будет работать из-за стирания типов. В случае же с массивами вы получите исключение во время выполнения в таких сценариях...
Таким образом, по сути, это поведение реализовано для того, чтобы нельзя было добавить неправильный элемент в коллекцию. Я полагаю, что стирание типов существует для обеспечения совместимости с устаревшим Java без использования обобщений.
Разница между <? super T> и <? extends T> в Java
Как создать обобщённый массив в Java?
Что такое PECS (Producer Extends Consumer Super)?
Как получить экземпляр класса обобщенного типа T?
Что значит 'synchronized'?