9

Является ли List<Собака> подклассом List<Животное>? Почему дженерики в Java не являются неявно полиморфными?

16

Я немного запутался в том, как дженерики в Java обрабатывают наследование и полиморфизм.

Предположим, у нас есть следующая иерархия:

Животное (родитель)

Собака - Кошка (дочерние классы)

Итак, у меня есть метод doSomething(List<Animal> animals). Согласно всем правилам наследования и полиморфизма, я бы предположил, что List<Dog> является List<Animal>, а List<Cat> тоже является List<Animal>, и поэтому любой из этих списков можно передать в этот метод. Но не тут-то было. Если я хочу достичь такого поведения, мне нужно явно указать методу принимать список любых подклассов Animal, написав doSomething(List<? extends Animal> animals).

Я понимаю, что это поведение Java. Мой вопрос в том, почему? Почему полиморфизм обычно является неявным, но когда дело доходит до дженериков, его нужно указывать явно?

5 ответ(ов)

10

Нет, 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>.

0

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

Тем не менее, многие ищут решение этой проблемы.

Так что решение данной задачи с версии Java 10 выглядит следующим образом:

List<S> supers = List.copyOf(descendants);

Эта функция выполнит приведение, если оно совершенно безопасно, или создаст копию, если приведение будет небезопасным. В результате получится неизменяемый список.

Для более глубокого объяснения (которое учитывает потенциальные подводные камни, упомянутые в других ответах) смотрите связанный вопрос и мой ответ на него за 2022 год: https://stackoverflow.com/a/72195980/773113.

0

Я бы сказал, что основная цель дженериков в том, что они не позволяют этого делать. Рассмотрим ситуацию с массивами, которые допускают такую типовую ковариантность:

Object[] objects = new String[10];
objects[0] = Boolean.FALSE;

Этот код компилируется без ошибок, но вызывает ошибку во время выполнения (java.lang.ArrayStoreException: java.lang.Boolean на второй строке). Это не типобезопасно. Цель дженериков — добавить безопасность типов на этапе компиляции, иначе можно было бы обойтись обычным классом без дженериков.

Некоторые ситуации требуют большей гибкости, и для этого предназначены ? super Class и ? extends Class. Первый используется, когда нужно вставить элемент в определенный тип Collection, а второй — когда нужно безопасно читать из него. Однако единственный способ сделать и то, и другое одновременно — иметь конкретный тип.

0

Ваш вопрос касается использования дженериков в 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.

Так что использование дженериков предоставляет гибкость, позволяя нам контролировать, какие типы могут быть переданы, и избегать потенциальных ошибок времени выполнения.

0

Логика такого поведения заключается в том, что Generics следуют механизму стирания типов. Таким образом, на этапе выполнения вы не имеете возможности определить тип collection, в отличие от массивов, где такой процесс стирания отсутствует. Возвращаясь к вашему вопросу...

Предположим, есть метод, как показано ниже:

add(List<Animal>){
    //Вы можете добавить List<Dog или List<Cat>, и это скомпилируется в соответствии с правилами полиморфизма
}

Теперь, если Java позволяет вызывающему коду добавлять List типа Animal в этот метод, то вы можете добавить неправильный элемент в коллекцию, и на этапе выполнения это код будет работать из-за стирания типов. В случае же с массивами вы получите исключение во время выполнения в таких сценариях...

Таким образом, по сути, это поведение реализовано для того, чтобы нельзя было добавить неправильный элемент в коллекцию. Я полагаю, что стирание типов существует для обеспечения совместимости с устаревшим Java без использования обобщений.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь