Я знаю, почему этого не следует делать. Но есть ли способ объяснить непрофессионалу, почему это невозможно. Вы можете легко объяснить это непрофессионалу: Animal animal = new Dog();
. Собака - это своего рода животное, но список собак - это не список животных.
Любой простой способ объяснить, почему я не могу сделать Список <Животное> animals = new ArrayList <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()
.......
Получил?