Я знаю, почему этого не следует делать. Но есть ли способ объяснить неспециалисту, почему это невозможно? Вы можете легко объяснить это неспециалисту: Animal animal = new Dog();
. Собака - это разновидность животного, но список собак - это не список животных.
Любой простой способ объяснить, почему я не могу делать Список ‹Animal› животные = новый ArrayList ‹Dog› ()?
Ответы (13)
Представьте, что вы составляете список Собак. Затем вы объявляете его как Список ‹Animal› и передаете его коллеге. Он небезосновательно считает, что может засунуть в него кота.
Затем он возвращает его вам, и теперь у вас есть список Собак с Кошкой в центре. Наступает хаос.
Важно отметить, что это ограничение существует из-за изменчивости списка. В Scala (например) вы можете объявить список Собак списком Животных. Это потому, что списки Scala (по умолчанию) неизменяемы, и поэтому добавление Cat к списку Dogs даст вам новый список < strong> Животные.
Ответ, который вы ищете, связан с концепциями ковариации и контравариантности. Некоторые языки поддерживают их (например, .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.
Лучший ответ непрофессионала, который я могу дать, следующий: потому что при разработке дженериков они не хотят повторять то же решение, которое было принято в отношении системы типов массивов Java, сделавшей ее небезопасной.
Это возможно с массивами:
Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();
Этот код компилируется отлично из-за того, как система типов массива работает в Java. Это вызовет ArrayStoreException
во время выполнения.
Было принято решение не допускать такого небезопасного поведения для дженериков.
См. Также в другом месте: Безопасность типов разрыва массивов Java, который многие считают одним из Недостатки дизайна Java.
Список ‹Animal› - это объект, куда вы можете вставить любое животное, например кошку или осьминога. ArrayList ‹Dog› - нет.
Вы пытаетесь сделать следующее:
List<? extends Animal> animals = new ArrayList<Dog>()
Это должно сработать.
Я бы сказал, что самый простой ответ - игнорировать кошек и собак, они не актуальны. Важен сам список.
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>
Предположим, вы можете сделать это. Одна из вещей, которую кто-то вручил List<Animal>
, разумно ожидать, - это добавить к ней Giraffe
. Что должно произойти, если кто-то попытается добавить Giraffe
к animals
? Ошибка времени выполнения? Казалось бы, это противоречит цели типизации во время компиляции.
Giraffe
, почему он выдаст ошибку времени выполнения? Если я сделаю animals.get()
, я могу ожидать только animal
, а Giraffe
будет animal
. Было бы странно вставлять Giraffe
в ArrayList<Dog>()
, но я не вижу ошибок времени выполнения. Все типы стираются во время выполнения.
- person fastcodejava; 27.02.2010
ArrayList<Dog>
, как в ответе Руны. Если нет, все было бы хорошо, правда?
- person fastcodejava; 27.02.2010
Обратите внимание, что если у вас есть
List<Dog> dogs = new ArrayList<Dog>()
тогда, если бы ты мог сделать
List<Animal> animals = dogs;
это не превращает dogs
в List<Animal>
. Структура данных, лежащая в основе животных, по-прежнему является ArrayList<Dog>
, поэтому, если вы попытаетесь вставить Elephant
в animals
, вы фактически вставите его в ArrayList<Dog>
, что не сработает (очевидно, что слон слишком велик ;-).
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] { ... }
Если бы вы не могли изменить список, то ваши рассуждения были бы совершенно разумными. К сожалению, 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>
, когда его интерфейс позволяет вам добавлять новые элементы в список.
Ответ на английском:
Если «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!!
Для того, чтобы иметь отношение подтипов, необходимо обеспечить соответствие критериям «применимости» / «заменяемости».
Подстановка легального объекта - все операции над предком поддерживаются на потомке:
// 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;
Недопустимая универсальная подстановка (приведение параметра типа) - неподдерживаемые операции в наследнике:
// 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
- (также, если параметр типа используется как в ковариантной, так и в контравариантной позициях, это приводит к инвариантности)
Наследуя, вы фактически создаете общий тип для нескольких классов. Вот вам и общий животный тип. вы используете его, создавая массив типа Animal и сохраняя значения аналогичных типов (унаследованные типы dog, cat и т. д.).
Eg:
dim animalobj as new List(Animal)
animalobj(0)=new dog()
animalobj(1)=new Cat()
.......
Понятно?