Любой простой способ объяснить, почему я не могу сделать List<Animal> animals = new ArrayList<Dog>()?
Я знаю, почему нельзя этого делать. Но есть ли способ объяснить непрофессионалу, почему это невозможно. Вы можете легко объяснить это неспециалисту: Animal animal = new Dog();
, Собака - это своего рода животное, но список собак - это не список животных.
13 ответов
Представьте, что вы создаете список собак. Затем вы объявляете это списком
Затем он возвращает его вам, и теперь у вас есть список собак, с котом в центре. Хаос наступает.
Важно отметить, что это ограничение существует из-за изменчивости списка. Например, в Scala вы можете объявить, что список собак - это список животных. Это потому, что списки Scala являются (по умолчанию) неизменяемыми, и поэтому добавление Cat в список Dogs даст вам новый список Animals.
Лучший ответ непрофессионала, который я могу дать, заключается в следующем: потому что при разработке дженериков они не хотят повторять то же решение, которое было принято с системой типов массивов Java, что делало ее небезопасной.
Это возможно с массивами:
Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();
Этот код прекрасно компилируется из-за того, как система типов массива работает в Java. Это поднимет ArrayStoreException
во время выполнения.
Было принято решение не допускать такого небезопасного поведения для дженериков.
Смотрите также в другом месте: http://c2.com/cgi-bin/wiki?JavaArraysBreakTypeSafety, который многие считают одним из недостатков Java Design.
Ответ, который вы ищете, связан с понятиями ковариации и контравариантности. Некоторые языки поддерживают их (например,.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 наследуется от animal, проверка во время компиляции предполагает, что его можно добавить в List. Но во время выполнения вы не можете добавить кота в список собак!
Таким образом, хотя это может показаться интуитивно простым, эти проблемы на самом деле очень сложные, с которыми приходится иметь дело.
Обзор MSDN о совместимости / контравариантности в.NET 4 приведен здесь: http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx - все это применимо и к Java, хотя я не не знаю, что такое поддержка Java.
То, что вы пытаетесь сделать, это следующее:
List<? extends Animal> animals = new ArrayList<Dog>()
Это должно работать.
Список
Я бы сказал, что самый простой ответ - игнорировать кошек и собак, они не имеют отношения к делу. Что важно, так это сам список.
List<Dog>
а также
List<Animal>
Это разные типы, которые собака получает от 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
? Ошибка времени выполнения? Казалось бы, это побеждает цель типизации во время компиляции.
Обратите внимание, что если у вас есть
List<Dog> dogs = new ArrayList<Dog>()
тогда, если бы вы могли сделать
List<Animal> animals = dogs;
это не получается dogs
в List<Animal>
, Структура данных, лежащих в основе животных, все еще ArrayList<Dog>
так что если вы попытаетесь вставить Elephant
в animals
вы фактически вставляете его в ArrayList<Dog>
что не сработает (Слон явно слишком большой;-).
Во-первых, давайте определимся с нашим животным миром:
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), то вы могли бы лечить 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. Это безопасные позиции для 2 случаев:
ковариантные позиции:
- возвращаемый тип метода (вывод универсального типа) - подтипы должны быть одинаково / более ограничивающими, поэтому их возвращаемые типы соответствуют предку
- тип неизменяемых полей (устанавливается классом владельца, затем "только для внутреннего вывода") - подтипы должны быть более строгими, поэтому, когда они устанавливают неизменяемые поля, они соответствуют предку
В этих случаях можно разрешить заменяемость параметра типа с таким потомком:
SomeCovariantType<Dog> decendant = new SomeCovariantType<>; SomeCovariantType<? extends Animal> ancestor = decendant;
Подстановочный знак плюс 'extends' дает ковариацию, указанную для сайта использования.
противоречивые позиции:
- тип параметра метода (входной в универсальный тип) - подтипы должны быть одинаково / более приспособлены, чтобы они не ломались при передаче параметров предка
- границы параметров верхнего типа (внутренняя реализация типа) - подтипы должны быть одинаково / более удобными, чтобы они не ломались, когда предки устанавливают значения переменных
В этих случаях можно разрешить замену параметра типа предком следующим образом:
SomeContravariantType<Animal> decendant = new SomeContravariantType<>; SomeContravariantType<? super Dog> ancestor = decendant;
Подстановочный знак плюс 'super' дает противоположность, указанную для сайта использования.
Использование этих двух идиом требует от разработчика дополнительных усилий и заботы, чтобы получить "способность к замене". Java требует ручного усилия разработчика, чтобы гарантировать, что параметры типа действительно используются в ковариантных / контравариантных позициях, соответственно (следовательно, типобезопасны). Я не знаю, почему - например, компилятор Scala проверяет это:-/. Вы в основном говорите компилятору: "Поверь мне, я знаю, что делаю, это безопасно для типов".
инвариантные позиции
- тип изменяемого поля (внутренний ввод и вывод) - может читаться и записываться всеми классами предков и подтипов - чтение ковариантно, запись контравариантна; результат инвариантен
- (также если параметр типа используется как в ковариантном, так и в контравариантном положениях, то это приводит к инвариантности)
Унаследовав, вы фактически создаете общий тип для нескольких классов. Здесь у вас есть общий тип животных. Вы используете его, создавая массив в типе Animal и сохраняя значения похожих типов (унаследованные типы dog, cat и т. д.).
Например:
dim animalobj as new List(Animal)
animalobj(0)=new dog()
animalobj(1)=new Cat()
.......
Понял?