Является ли List<Dog> подклассом List<Animal>? Почему дженерики в Java не полиморфны?
Краткий ответ: нет, List<Dog> не является подклассом List<Animal>
Это один из самых распространённых вопросов у новичков в Java. Интуитивно кажется, что раз Dog — это Animal, то и список собак должен быть списком животных. Но в Java дженерики инвариантны (invariant). Это значит, что List<Dog> и List<Animal> — это два совершенно разных, не связанных типа, даже несмотря на наследование между Dog и Animal.
Почему так сделано? — Безопасность типов
Представьте, что Java разрешила бы такое присваивание:
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // Гипотетически разрешено
animals.add(new Cat("Tom")); // Добавляем кота в список собак!
Dog myDog = dogs.get(0); // ClassCastException! В списке лежит Cat
Если бы List<Dog> был подклассом List<Animal>, мы могли бы добавить в список собак кота (через ссылку на список животных). А потом, когда мы достаём элемент из списка собак, мы ожидаем Dog, а получаем Cat — и ловим исключение в рантайме. Дженерики были созданы именно для того, чтобы предотвратить такие ошибки на этапе компиляции. Поэтому компилятор запрещает такое преобразование.
Как правильно решить задачу? — Wildcards (маски)
Для случаев, когда нам нужно работать с любым подтипом, Java предоставляет wildcards (символ подстановки ?). Чтобы метод мог принимать список любых животных (и их подтипов), используйте верхнюю границу (upper bound):
public static void printAnimalNames(List<? extends Animal> animals) {
for (Animal a : animals) {
System.out.println(a.getName());
}
}
Теперь метод принимает List<Dog>, List<Cat> и даже List<Animal>. Но есть важное ограничение: вы не можете добавлять элементы в такой список (кроме null). Компилятор не знает, какой конкретно тип хранится в списке, поэтому он блокирует запись, чтобы не нарушить безопасность типов.
Другие варианты wildcards
List<? extends Animal>— список неизвестного типа, который является Animal или его подтипом (Producer — только читаем).List<? super Dog>— список неизвестного типа, который является Dog или его супертипом (Consumer — можно писать Dog и его подтипы).List<?>— список любого типа (unbounded wildcard).
Это правило называют PECS (Producer Extends, Consumer Super): если вы только читаете из коллекции — используйте extends; если только пишете — используйте super.
Итог
Дженерики не полиморфны, потому что они должны гарантировать безопасность типов во время компиляции. Если вам нужен полиморфизм для коллекций — используйте wildcards. В вашем случае правильное решение — List<? extends Animal>.
Альтернативный взгляд: дженерики и массивы
Интересно, что массивы в Java ведут себя иначе. Массив Dog[] является подтипом Animal[]. Это называется ковариантность (covariance). То есть:
Dog[] dogs = new Dog[2];
Animal[] animals = dogs; // Работает!
animals[0] = new Cat(); // Ошибка в рантайме: ArrayStoreException
Массивы "помнят" свой конкретный тип при создании (Dog[]), и при попытке положить Cat выбрасывают исключение. Дженерики же были спроектированы так, чтобы такие ошибки ловились на этапе компиляции. Поэтому для дженериков выбрали инвариантность — это более строгий, но безопасный подход.
Практический совет
Если вам нужно передавать коллекции разных типов в метод, всегда используйте wildcards. Если метод только читает данные — ? extends T. Если только записывает — ? super T. Это сделает ваш код гибким и типобезопасным.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги