Почему нельзя перечислить‹? extends Animal› заменить на List‹Animal›?

Рассмотрим следующий код:

public class Main {

    static class Animal {}

    static class Dog extends Animal {}

    static List<? extends Animal> foo() {
        List<Dog> dogs = new ArrayList<>();
        return dogs;
    }

    public static void main(String[] args) {
        List<Animal> dogs = Main.foo(); // compile error
    }
}

Я пытаюсь понять, почему он не скомпилируется. То есть, почему компилятор не позволяет мне ссылаться на List<? extends Animal> как на List<Animal>? Это как-то связано с механизмом стирания типов?


person yaseco    schedule 04.08.2020    source источник


Ответы (3)


List<Animal> – это List, к которому вы можете добавить любой Animal (или null), и все, что вы из него уберете, будет Animal.

List<? extends Animal> — это список, который содержит только определенный подкласс Animal (или null), и вы не знаете, какой именно; это позволяет вам рассматривать все, что вы берете из него, как Animal, но вам не разрешено ничего к нему добавлять (кроме буквального null).


List<? extends Animal> не может действовать как List<Animal>, потому что это позволит вам сделать следующее:

List<Cat> listOfCats = new ArrayList<>();
List<? extends Animal> listOfSomeAnimals = listOfCats;  // Fine.
List<Animal> listOfAnimals = listOfSomeAnimals;  // Error, pretend it works.
listOfAnimals.add(new Dog());

Теперь, поскольку listOfCats, listOfSomeAnimals и listOfAnimals — это один и тот же список, Dog было добавлено к listOfCats. Как таковой:

Cat cat = listOfCats.get(0);  // ClassCastException.
person Andy Turner    schedule 04.08.2020
comment
На самом деле вы не можете ничего добавить к List<? extends Animal>, кроме нуля. - person dpr; 04.08.2020
comment
@dpr, это был мой следующий вопрос :) - person yaseco; 04.08.2020
comment
@yaseco, см. stackoverflow.com/questions/55514277/ - person dpr; 04.08.2020
comment
@dpr на самом деле, вы даже можете добавлять элементы, отличные от null, когда они происходят из одного и того же списка. Вы не можете сделать это непосредственно в списке с подстановочным знаком, но рассмотрите такой метод, как static <T> void dupElements( List<T> list) { for(ListIterator<T> i = list.listIterator(); i.hasNext(); ) i.add(i.next()); }, который будет добавлять элементы из того же списка. Вы можете передать ему свой List<? extends Animal> без проблем, так как это не нарушит безопасность типов. - person Holger; 05.08.2020
comment
Кстати, законно сделать List<Animal> list = Collections.unmodifiableList(listOfSomeAnimals);, чтобы получить представление того же списка без типа элемента подстановки, поскольку это представление предотвращает вставку неподходящих элементов другими способами. Точно так же, когда вы делаете List<Dog> dogs = List.of(new Dog(), new Dog(), new Dog()); List<Animal> listOfSomeAnimals = List.copyOf(dogs); System.out.println(dogs == (Object)listOfSomeAnimals);, он напечатает true, поскольку для неизменяемых списков нет конфликта типов (copyOf не копирует уже неизменяемый список в качестве оптимизации). - person Holger; 05.08.2020

Потому что List<? extends Animal> разрешил бы любой подкласс Animal. List просто разрешил бы объекты класса Animal.

В List<? extends Animal> также разрешены такие объекты, как кошка или собака. Если вы инициируете это с помощью чистого списка собак, вы не сможете сказать извне, что это не разрешено, и поэтому оно не скомпилируется.

person Scruples    schedule 04.08.2020

Ко-, контра- и инвариантность в Java

Речь идет о Со-, Контра- и Инвариантности. Ковариантность говорит нам о том, что мы можем убрать, контравариантность — о том, что мы можем добавить, а инвариантность — о том и другом.

Инвариантность

List<Animal> является инвариантным. Вы можете добавить любое Животное, и вы гарантированно получите любое Животное — get(int) дает нам Animal, а add(Animal) должно принять любое Животное. Мы можем поместить Животное, мы получим Животное.

List<Animal> animals = new ArrayList<Dog>() является ошибкой компилятора, поскольку он не принимает Animal или Cat. get(int) по-прежнему дает нам только животных (в конце концов, собаки — это животные), но не принимать других — это нарушение условий сделки.

List<Animal> animals = new ArrayList<Object>() также является нарушителем условий сделки. Да, он принимает любое животное (мы можем поставить Животных), но дает нам Объекты.

Контравариантность

List<? super Dog> является контравариантным. Мы можем только ввести собак, ничего не сказано о том, что мы получим. Таким образом, мы получаем Object.

List<? super Dog> dogs = new ArrayList<Animal>(); это работает, потому что мы можем поместить в него собаку. А Животные — это Объекты, поэтому мы можем доставать объекты.

List<? super Dog> dogs = new ArrayList<Animal>();
// dogs.add(new Animal()); // compile error, need to put Dog in
dogs.add(new Dog());
Object obj = dogs.get(0);
// Dog dog = dogs.get(0); // compile error, can only take Object out

Ковариация

List<? extends Animal> является ковариантным. Вы гарантированно получите животное.

List<? extends Animal> animals = new ArrayList<Cat>(); работает, потому что кошки — это животные, а get(n) дает вам животных. Конечно, все они кошки, но кошки — животные, так что это работает нормально.

Однако добавлять элементы сложнее, поскольку на самом деле у вас нет типа, который вы можете вставить:

List<? extends Animal> animals = new ArrayList<Cat>();
//animals.add(new Cat()); // compile error
//animals.add(new Animal()); // compile error
Animal animal = animals.get(0);

List<? extends Cat> cats = new ArrayList<Animal>(); является ошибкой компилятора, потому что вы можете убрать любое животное, но вы требуете, чтобы единственное, что можно было убрать, это кошки.

Ваш код

static List<? extends Animal> foo() {
    List<Dog> dogs = new ArrayList<>();
    return dogs;
}

Здесь все в порядке. foo() – это список, из которого вы можете выносить животных. Вы, конечно, Поскольку Собаки - Животные, и вы можете убить Собак, вы можете убить Животных. Все, что вы уберете из Списка, гарантированно будет Животным.

List<Animal> dogs = Main.foo(); // compile error

Вы говорите, что dogs — это список, в который вы можете вставить любое Animal, и вы гарантированно получите животных. Последняя часть проста, да, вы гарантированно получите Животных, вот что означает ? extends Animal. Но вы не можете вставлять произвольные Животные. Вот почему это не работает.

person Polygnome    schedule 04.08.2020
comment
Ваш ответ дает много смысла, и он познакомил меня с новыми понятиями. Благодарю вас! - person yaseco; 04.08.2020