Любой простой способ объяснить, почему я не могу сделать Список <Животное> animals = new ArrayList <Dog>()?

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

Ответ 1

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

Затем он возвращает его вам, и теперь у вас есть список Собаки, с Cat в середине. Возникает хаос.

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

Ответ 2

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

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 будет происходить от животного, проверка времени компиляции предполагает, что его можно добавить в список. Но во время выполнения вы не можете добавить Cat в список собак!

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

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

Ответ 3

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

Это возможно с помощью массивов:

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

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

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

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

Ответ 4

Что вы пытаетесь сделать, это следующее:

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

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

Ответ 5

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

Ответ 6

Предположим, вы могли это сделать. Одна из вещей, которые кто-то передал List<Animal>, мог бы разумно ожидать, что сможет сделать это, чтобы добавить к нему Giraffe. Что произойдет, если кто-то попытается добавить Giraffe в animals? Ошибка времени выполнения? Это, казалось бы, превзошло цель печатания времени компиляции.

Ответ 7

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

List<Dog> 

и

List<Animal> 

- разные типы, которые Собака извлекает из Животного, не имеет никакого отношения к этому вообще.

Этот оператор недействителен

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

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

AnimalList dogs = new DogList();

Пока собака может наследовать от Animal, класс списка, сгенерированный

List<Animal> 

не наследуется от класса списка, сгенерированного

List<Dog>

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

List<Animal>

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

List<Dog> 

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

List<Animal>

Ответ 8

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

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

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

List<Animal> animals = dogs;

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

Ответ 9

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

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 - Dog.

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:

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.

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 автоматически разрешает Container<Dog> присваиваться Container<Animal>, можно также ожидать, что a Comparator<Dog> может быть присвоено Comparator<Animal>, что не имеет смысла - как бы Comparator<Dog> сравнить два Кошки?

В чем разница между Container и Comparator? Контейнер создает значения типа 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 по умолчанию использует инвариантность. Вы должны явно выбрать соответствующую дисперсию с помощью ? extends X или ? super X для типов переменных, полей, параметров или методов.

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

Это называется "Отклонение от объявлений" и доступно в Scala.

trait Container[+T] { ... }
trait Comparator[-T] { ... }

Ответ 10

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

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

Это пример более общего Принципа подписи Liskov.

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

Является ли Square a Rectangle? Конечно - с математической точки зрения.

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

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

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

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

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

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

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

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

Ответ 11

Это связано с тем, что общие типы invariant.

Ответ 12

Русский Ответ:

Если 'List<Dog> является List<Animal>', первый должен поддерживать (наследовать) все операции последнего. Добавление кошки можно сделать последним, но не прежним. Таким образом, отношения "есть" не срабатывают.

Ответ на программирование:

Тип Безопасность

Конкретный выбор дизайна по умолчанию, который останавливает это повреждение:

List<Dog> dogs = new List<>();
dogs.add(new Dog("mutley"));
List<Animal> animals = dogs;
animals.add(new Cat("felix"));  
// Yikes!! animals and dogs refer to same object.  dogs now contains a cat!!

Для того чтобы иметь отношение подтипа, необходимо устранить критерии "castability" /'substitability'.

  • Подменю юридического объекта - все операции над предок, поддерживаемые на decendant:

    // Legal - one object, two references (cast to different type)
    Dog dog = new Dog();
    Animal animal = dog;  
    
  • Замена юридической коллекции - все операции над предком, поддерживаемые потомком:

    // Legal - one object, two references (cast to different type)
    List<Animal> list = new List<Animal>()
    Collection<Animal> coll = list;  
    
  • Незаконная общая подстановка (тип параметра) - неподдерживаемые операционные системы в decendant:

    // Illegal - one object, two references (cast to different type), but not typesafe
    List<Dog> dogs = new List<Dog>()
    List<Animal> animals = list;  // would-be ancestor has broader ops than decendant
    

Однако

В зависимости от конструкции универсального класса параметры типа могут использоваться в "безопасных положениях", что означает, что литье/подстановка иногда может быть успешной без искажения безопасности типа. Ковариация означает, что общее приспособление G<U> может подставлять G<T>, если U является одним и тем же типом или подтипом T. Контравариантность означает, что общее событие G<U> может подставлять G<T>, если U является одним и тем же типом или супертипом T. Это безопасный позиции для двух случаев:

  • ковариантные позиции:

    • тип возвращаемого метода (вывод родового типа) - подтипы должны быть одинаково/более ограничительными, поэтому их типы возвращаемости соответствуют предку
    • тип неизменяемых полей (задается классом владельца, а затем только внутренним выходом) - подтипы должны быть более ограничительными, поэтому, когда они устанавливают неизменяемые поля, они соответствуют предкам

    В этих случаях безопасно допускать замену параметра типа с таким декомпенсацией:

    SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
    SomeCovariantType<? extends Animal> ancestor = decendant;
    

    Подстановочный знак плюс 'extends' дает указанную ковариацию с использованием сайта.

  • contrvariant position:

    • тип параметра метода (ввод в общий тип) - подтипы должны быть одинаково/более приспособлены, чтобы они не прерывались при передаче параметров предка
    • ограничения параметра верхнего уровня (внутреннее создание экземпляра) - подтипы должны быть одинаково/более приспособлены, поэтому они не прерываются, когда предки устанавливают значения переменных

    В этих случаях безопасно допускать замену параметра типа с таким предком:

    SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
    SomeContravariantType<? super Dog> ancestor = decendant;
    

    Подстановочный знак плюс 'супер' дает указанную контравариантность на сайте.

Использование этих двух идиом требует дополнительных усилий и забот от разработчика, чтобы получить "силу замещаемости". Java требует ручного усилия разработчиков, чтобы гарантировать, что параметры типа действительно используются в ковариантных/контравариантных позициях соответственно (следовательно, безопасны по типу). Я не знаю, почему. Компилятор scala проверяет это: -/. Вы в основном говорите компилятору "поверьте мне, я знаю, что я делаю, это безопасно для типов".

  • инвариантные позиции

    • тип изменяемого поля (внутренний вход и выход) - может считываться и записываться всеми классами предков и подтипов - чтение ковариантно, запись контравариантна; результат является инвариантным
    • (также, если параметр типа используется как в ковариантной, так и в контравариантной позициях, то это приводит к инвариантности)

Ответ 13

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

Например:

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

.......

Получил?