Какой-либо простой способ объяснить, почему я не могу сделать Списка <Животное> животные = новый ArrayList <Собака> ()? [дубликат]

Этот вопрос уже имеет ответ здесь:

Я знаю, почему не нужно делать этого. Но есть ли способ объяснить неспециалисту, почему это не возможно. Можно объяснить это неспециалисту легко: Animal animal = new Dog();. Собака является своего рода животным, но список собак не является списком животных.

22
задан Carl Manaster 27 February 2010 в 15:54
поделиться

12 ответов

Представьте, что вы создали список собак . Затем вы объявляете его как List и передаете его коллеге. Он, небезосновательно , полагает, что может поместить в него кота .

Затем он возвращает его вам, и теперь у вас есть список собак с кошкой в центре. Наступает хаос.

Важно отметить, что это ограничение существует из-за изменчивости списка. В Scala (например) вы можете объявить, что список собак является списком животных .Это потому, что списки Scala (по умолчанию) неизменяемы, и поэтому добавление Cat в список Dogs даст вам новый список Animals .

48
ответ дан 29 November 2019 в 03:25
поделиться

Это потому, что универсальные типы инвариантны .

1
ответ дан 29 November 2019 в 03:25
поделиться

Сначала давайте определим наше царство животных:

interface Animal {
}

class Dog implements Animal{
    Integer dogTag() {
        return 0;
    }
}

class Doberman extends Dog {        
}

Рассмотрим два параметризованных интерфейса:

interface Container<T> {
    T get();
}

interface Comparator<T> {
    int compare(T a, T b);
}

И их реализации, где T - Собака .

class DogContainer implements Container<Dog> {
    private Dog dog;

    public Dog get() {
        dog = new Dog();
        return dog;
    }
}

class DogComparator implements Comparator<Dog> {
    public int compare(Dog a, Dog b) {
        return a.dogTag().compareTo(b.dogTag());
    }
}

То, что вы спрашиваете, вполне разумно в контексте этого интерфейса контейнера :

Container<Dog> kennel = new DogContainer();

// Invalid Java because of invariance.
// Container<Animal> zoo = new DogContainer();

// But we can annotate the type argument in the type of zoo to make
// to make it co-variant.
Container<? extends Animal> zoo = new DogContainer();

Так почему же Java не делает это автоматически? Подумайте, что это будет значить для Компаратора .

Comparator<Dog> dogComp = new DogComparator();

// Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats!
// Comparator<Animal> animalComp = new DogComparator();

// Invalid Java, because Comparator is invariant in T
// Comparator<Doberman> dobermanComp = new DogComparator();

// So we introduce a contra-variance annotation on the type of dobermanComp.
Comparator<? super Doberman> dobermanComp = new DogComparator();

Если Java автоматически разрешила назначить Контейнер Контейнер , можно было бы также ожидать, что Comparator может быть назначен Comparator , что не имеет смысла - как Comparator может сравнивать двух Cats?

Так в чем же разница между Container и Компаратор ? Контейнер производит значений типа T , тогда как Comparator потребляет их. Они соответствуют ковариантному и контрвариантному использованию параметра типа.

Иногда параметр типа используется в обеих позициях, что делает интерфейс инвариантным .

interface Adder<T> {
   T plus(T a, T b);
}

Adder<Integer> addInt = new Adder<Integer>() {
   public Integer plus(Integer a, Integer b) {
        return a + b;
   }
};
Adder<? extends Object> aObj = addInt;
// Obscure compile error, because it there Adder is not usable
// unless T is invariant.
//aObj.plus(new Object(), new Object());

По причинам обратной совместимости Java по умолчанию использует инвариантность .Вы должны явно выбрать подходящую дисперсию с ? расширяет X или ? super X о типах переменных, полей, параметров или возвращаемых методов.

Это настоящая проблема - каждый раз, когда кто-то использует общий тип, он должен принять это решение! Несомненно, авторы Контейнера и Компаратора должны иметь возможность заявить об этом раз и навсегда.

Это называется «Изменение сайта объявления» и доступно в Scala.

trait Container[+T] { ... }
trait Comparator[-T] { ... }
2
ответ дан 29 November 2019 в 03:25
поделиться

Вы пытаетесь сделать следующее:

List<? extends Animal> animals = new ArrayList<Dog>()

Это должно сработать.

5
ответ дан 29 November 2019 в 03:25
поделиться

A Список - это объект, в который можно вставить любое животное, например кошку или осьминога. ArrayList - нет.

5
ответ дан 29 November 2019 в 03:25
поделиться

Ответ, который вы ищете, связан с такими понятиями, как ковариация и контравариантность. Некоторые языки поддерживают их (например, .NET 4 добавляет поддержку), но некоторые из основных проблем демонстрируются таким кодом:

List<Animal> animals = new List<Dog>();

animals.Add(myDog); // works fine - this is a list of Dogs
animals.Add(myCat); // would compile fine if this were allowed, but would crash!

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

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

Здесь есть обзор совместной / контравариантности в .NET 4 в MSDN: http://msdn.microsoft.com/en-us/library/dd799517 (VS.100) .aspx - это все применимо и к java, хотя я не знаю, что такое поддержка Java.

8
ответ дан 29 November 2019 в 03:25
поделиться

Предположим, вы можете сделать это. Одна из вещей, которые кто-то передал List , как разумно ожидать, - это добавить к нему Жирафа . Что должно произойти, если кто-то попытается добавить Жирафа к животным ? Ошибка времени выполнения? Казалось бы, это противоречит цели типизации во время компиляции.

3
ответ дан 29 November 2019 в 03:25
поделиться

При наследовании вы фактически создаете общий тип для нескольких классов. Здесь у вас есть общий тип животного. Вы используете его, создавая массив в типе Animal и сохраняя значения похожих типов (унаследованные типы dog, cat, etc.).

Например:

 dim animalobj as new List(Animal)
  animalobj(0)=new dog()
   animalobj(1)=new Cat()

.......

Понятно?

-1
ответ дан 29 November 2019 в 03:25
поделиться

Если вы не можете изменить список, то ваше рассуждение будет совершенно правильным. К сожалению, List <> манипулируется императивно.Это означает, что вы можете изменить List , добавив к нему новое Animal . Если вам разрешили использовать List как List , вы могли бы получить список, который также содержит Cat .

Если List <> не может быть изменен (как в Scala), то вы можете рассматривать A List как List . Например, C # делает возможным такое поведение с помощью аргументов типа ковариантных и контравариантных обобщенных типов.

Это пример более общего принципа подстановки Лискова .

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

Является ли квадрат прямоугольником ? Конечно, с математической точки зрения.

Вы можете определить класс Rectangle , который предлагает читаемые свойства getWidth и getHeight .

Вы даже можете добавить методы, которые вычисляют его площадь или периметр на основе этих свойств.

Затем вы можете определить класс Square , который наследует Rectangle и заставляет getWidth и getHeight возвращать одно и то же значение.

Но что произойдет, если вы начнете разрешать мутацию через setWidth или setHeight ?

Теперь Square больше не является разумным подклассом Rectangle .Мутация одного из этих свойств должна молча изменить другое, чтобы поддерживать инвариант, и принцип замещения Лискова будет нарушен. Изменение ширины квадрата может иметь неожиданный побочный эффект. Чтобы оставаться квадратом, вам также нужно изменить высоту, но вы попросили изменить только ширину!

Вы не можете использовать свой Квадрат , если бы вы могли использовать Прямоугольник . Итак, при наличии мутации Квадрат не является Прямоугольником !

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

Аналогично List не может быть List , когда его интерфейс позволяет добавлять новые элементы в список.

2
ответ дан 29 November 2019 в 03:25
поделиться

Обратите внимание, что если у вас есть

List<Dog> dogs = new ArrayList<Dog>()

то, если бы вы могли сделать

List<Animal> animals = dogs;

то это не превращает собак в List . Структура данных, лежащая в основе животных, по-прежнему является ArrayList, поэтому если вы попытаетесь вставить слона в животных, вы фактически вставите его в ArrayList, что не сработает (слон явно слишком большой ;-)).

2
ответ дан 29 November 2019 в 03:25
поделиться

Лучший непрофессиональный ответ, который я могу дать, таков: потому что при разработке generics они не хотят повторить то же решение, которое было принято в отношении системы типов массивов Java и сделало ее небезопасной.

Это возможно с массивами:

Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();

Этот код компилируется просто отлично из-за того, как система типов массивов работает в Java. Он вызовет ArrayStoreException во время выполнения.

Было принято решение не допускать такого небезопасного поведения для дженериков.

См. также в другом месте: Java Arrays Break Type Safety, который многие считают одним из Java Design Flaws.

8
ответ дан 29 November 2019 в 03:25
поделиться

Я бы сказал, что самый простой ответ - игнорировать кошек и собак, они не имеют значения. Важен сам список.

List<Dog> 

и

List<Animal> 

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

Это утверждение недействительно

List<Animal> dogs = new List<Dog>();

по той же причине, что и это

AnimalList dogs = new DogList();

Хотя Dog может наследоваться от Animal, класс list, порожденный

List<Animal> 

не наследуется от класса list, порожденного

List<Dog>

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

List<Animal>

это не означает, что

List<Dog> 

является подклассом

List<Animal>
4
ответ дан 29 November 2019 в 03:25
поделиться
Другие вопросы по тегам:

Похожие вопросы: