Любой простой способ объяснить, почему я не могу делать Список ‹Animal› животные = новый ArrayList ‹Dog› ()?

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


person fastcodejava    schedule 27.02.2010    source источник
comment
blogs.msdn.com/ericlippert/archive/tags/   -  person Andreas Grech    schedule 03.03.2010


Ответы (13)


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

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

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

person Brian Agnew    schedule 27.02.2010
comment
Мне нравится этот, потому что собаки и кошки живут вместе отсылки. Хотя я думаю, что гарантии, вероятно, должны последовать. Примечание: как только этот ответ будет полностью понят, это идеальный вход в какой список ‹? extends Animal ›предназначен, и его ограничения на то, какие методы вы можете вызывать, становятся более очевидными (например: нельзя вызывать add (), но можно вызывать get () и т. д.) - person PSpeed; 27.02.2010
comment
Действительно, Ensues. Сейчас отредактировал. - person Brian Agnew; 27.02.2010
comment
Кроме того, этот ответ, кажется, единственный, который описал его где-то близко к способу объяснения неспециалисту. В то время как я указываю на возможные опечатки, для выделенного жирным шрифтом списка во втором предложении может потребоваться какая-то атрибуция животных. - person PSpeed; 27.02.2010
comment
Спасибо. Заблудился в форматировании! - person Brian Agnew; 27.02.2010
comment
Хороший ответ, принятый, поскольку «наступает хаос». - person fastcodejava; 01.03.2010

Ответ, который вы ищете, связан с концепциями ковариации и контравариантности. Некоторые языки поддерживают их (например, .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.

person Dan Puzey    schedule 27.02.2010

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

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

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

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

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

См. Также в другом месте: Безопасность типов разрыва массивов Java, который многие считают одним из Недостатки дизайна Java.

person polygenelubricants    schedule 27.02.2010

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

person Thomas Padron-McCarthy    schedule 27.02.2010

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

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

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

person Reverend Gonzo    schedule 27.02.2010
comment
Объясняет ли это неспециалисту? Я так не думаю. - person Brian Agnew; 27.02.2010

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

List<Dog> 

и

List<Animal> 

являются разными типами, то есть, что Собака происходит от Животного, к этому не имеет никакого отношения.

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

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

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

AnimalList dogs = new DogList();

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

List<Animal> 

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

List<Dog>

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

List<Animal>

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

List<Dog> 

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

List<Animal>
person Ramon Leon    schedule 03.03.2010

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

person AakashM    schedule 27.02.2010
comment
Если я добавлю к нему Giraffe, почему он выдаст ошибку времени выполнения? Если я сделаю animals.get(), я могу ожидать только animal, а Giraffe будет animal. Было бы странно вставлять Giraffe в ArrayList<Dog>(), но я не вижу ошибок времени выполнения. Все типы стираются во время выполнения. - person fastcodejava; 27.02.2010
comment
Ошибка выполнения возникнет из-за другого кода, который, возможно, все еще использует ваш ArrayList ‹Dog› и правильно ожидает, что он содержит только Dogs. Кроме того, с теоретической точки зрения стирание - это деталь реализации в этом контексте. - person PSpeed; 27.02.2010
comment
@PSpeed ​​- Да, это правда, если у вас есть отдельная ссылка на ArrayList<Dog>, как в ответе Руны. Если нет, все было бы хорошо, правда? - person fastcodejava; 27.02.2010
comment
Тогда зачем объявлять ArrayList ‹Dog› вместо ArrayList ‹Animal›. Иначе вы подрываете систему набора текста ... и в чем тогда смысл? :) - person PSpeed; 27.02.2010
comment
... и кроме того, как я уже сказал, тот факт, что Java стирает тип ArrayList, является деталью реализации. Если какой-то другой класс Foo на самом деле не стирает свой тип, потому что, возможно, он берет его на конструктор, тогда он может проверять типы и выдавать вам ошибки во время выполнения. Тот факт, что Java не предоставляет действительно типобезопасных коллекций, в основном является вопросом обратной совместимости. - person PSpeed; 27.02.2010

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

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

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

List<Animal> animals = dogs;

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

person Rune    schedule 27.02.2010
comment
Да, это будет правдой, если у вас есть отдельная ссылка на ArrayList<Dog>. Если нет, все было бы хорошо, правда? - person fastcodejava; 27.02.2010

Во-первых, давайте определимся с нашим животным миром:

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>, можно было бы также ожидать, что 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] { ... }
person retronym    schedule 27.02.2010

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

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

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

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

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

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

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

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

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

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

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

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

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

person Edward KMETT    schedule 03.03.2010

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

person helpermethod    schedule 27.02.2010

Ответ на английском:

Если «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!!

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

  1. Подстановка легального объекта - все операции над предком поддерживаются на потомке:

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

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

    // 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. Это безопасные позиции для двух случаев:

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

    • method return type (output of generic type) - subtypes must be equally/more restrictive, so their return types comply with ancestor
    • тип неизменяемых полей (устанавливается классом-владельцем, затем «только для внутреннего вывода») - подтипы должны быть более строгими, поэтому, когда они устанавливают неизменяемые поля, они соответствуют предку

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

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

    Подстановочный знак плюс «расширяет» дает указанную на сайте ковариацию.

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

    • method parameter type (input to generic type) - subtypes must be equally/more accommodating so they don't break when passed parameters of ancestor
    • верхние границы параметра типа (создание внутреннего типа) - подтипы должны быть одинаково / более приспособленными, чтобы они не нарушались, когда предки устанавливают значения переменных.

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

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

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

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

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

    • type of mutable field (internal input and output) - can be read and written by all ancestor and subtype classes - reading is covariant, writing is contravariant; result is invariant
    • (также, если параметр типа используется как в ковариантной, так и в контравариантной позициях, это приводит к инвариантности)
person Glen Best    schedule 24.07.2013

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

Eg:

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

.......

Понятно?

person Vibin Jith    schedule 27.02.2010