Java Generics: не может использовать List <SubClass> для списка <SuperClass>?

Просто сталкивайтесь с этой проблемой:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // compile error: incompatible type

Если тип DataNode является подтипом дерева.

public class DataNode implements Tree

К моему удивлению, это работает для массива:

DataNode[] a2 = new DataNode[0];
Tree[] b2 = a2;   // this is okay

Это немного странно. Может ли кто-нибудь дать объяснение по этому поводу?

Ответ 1

То, что вы видите во втором случае, - это ковариация массива. Это плохой ИМО, который делает назначения в массиве небезопасными - они могут выйти из строя во время выполнения, несмотря на то, что время компиляции прекрасное.

В первом случае представьте, что код выполнил компиляцию и за ним следуют:

b1.add(new SomeOtherTree());
DataNode node = a1.get(0);

Что бы вы ожидали?

Вы можете сделать это:

List<DataNode> a1 = new ArrayList<DataNode>();
List<? extends Tree> b1 = a1;

... потому что тогда вы можете получать только вещи из b1, и они гарантированно совместимы с Tree. Вы не можете вызвать b1.add(...) именно потому, что компилятор не будет знать, безопасно это или нет.

Посмотрите этот раздел часто задаваемых вопросов по Java Generics Angelika Langer.

Ответ 2

Краткое объяснение: было ошибкой разрешить его изначально для массивов.

Более длинное объяснение:

Предположим, что это разрешено:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // pretend this is allowed

Тогда я не мог перейти к:

b1.add(new TreeThatIsntADataNode()); // Hey, b1 is a List<Tree>, so this is fine

for (DataNode dn : a1) {
  // Uh-oh!  There stuff in a1 that isn't a DataNode!!
}

Теперь идеальное решение позволит вам выбрать какой-либо тип при использовании варианта List, который был доступен только для чтения, но запретил бы его при использовании интерфейса (например, List), который читает-пишет. Java не допускает такого рода обозначения вариаций для параметров generics (*), но даже если бы это было так, вы не смогли бы отличить List<A> до List<B>, если A и B не были идентичны.

(*) То есть, это не допускается при написании классов. Вы можете объявить свою переменную типом List<? extends Tree>, и это прекрасно.

Ответ 3

Если вам нужно отбрасывать от List<DataNode> до List<Tree>, и вы знаете, что это безопасно, тогда уродливый способ добиться этого - сделать двойной бросок:

List<DataNode> a1 = new ArrayList<DataNode>();

List<Tree> b1 = (List<Tree>) (List<? extends Tree>) a1;

Ответ 4

List<DataNode> не распространяется List<Tree>, хотя DataNode расширяет Tree. Это потому, что после вашего кода вы можете сделать b1.add(SomeTreeThatsNotADataNode), и это будет проблемой, так как тогда a1 будет иметь элемент, который также не является DataNode.

Вам нужно использовать подстановочный знак для достижения чего-то вроде этого

List<DataNode> a1 = new ArrayList<DataNode>();
List<? extends Tree> b1 = a1;
b1.add(new Tree()); // compiler error, instead of runtime error

С другой стороны, DataNode[] ПРОДОЛЖАЕТСЯ Tree[]. В то время казалось логичным, но вы можете сделать что-то вроде:

DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2; // this is okay
b2[0] = new Tree(); // this will cause ArrayStoreException since b2 is actually a DataNode[] and can't store a Tree

Вот почему, когда они добавили generics в Collections, они решили сделать это немного иначе, чтобы предотвратить ошибки времени выполнения.

Ответ 5

Когда были созданы массивы (т.е. в значительной степени при разработке Java) разработчики решили, что дисперсия будет полезна, поэтому они разрешили ее. Однако это решение часто критиковали, потому что оно позволяет вам это сделать (предположим, что NotADataNode является другим подклассом Tree):

DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2;   // this is okay
b2[0] = new NotADataNode(); //compiles fine, causes runtime error

Итак, когда были разработаны дженерики, было решено, что общие структуры данных должны допускать только явное отклонение. То есть вы не можете сделать List<Tree> b1 = a1;, но вы можете сделать List<? extends Tree> b1 = a1;.

Однако, если вы сделаете последнее, попытка использовать метод add или set (или любой другой метод, который принимает T в качестве аргумента), приведет к ошибке компиляции. Таким образом невозможно скомпилировать эквивалент проблемы с указанным выше массивом (без опасных методов).

Ответ 6

Короткий ответ: Список a1 - это не тот же тип, что и список b2; В a1 вы можете поместить любой объект типа, который расширяет DataNode. Поэтому он может содержать другие типы, кроме дерева.

Ответ 8

Это ответ от С#, но я думаю, что на самом деле это не имеет никакого значения здесь, поскольку причина та же.

"В частности, в отличие от типов массивов, построенные ссылочные типы не показывают" ковариантные "преобразования. Это означает, что тип List <B> не имеет никакого преобразования (неявного или явного) в List <A> , даже если B является производным от A. Аналогично, никакого преобразования не существует из List <B> to List <object> .

Обоснование для этого простое: если разрешено преобразование в List <A> , то, по-видимому, можно хранить значения типа A в списке. Но это нарушит инвариант, что каждый объект в списке типа List <B> всегда является значением типа B, иначе могут возникнуть непредвиденные сбои при назначении классам коллекции. "

http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e

Ответ 9

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

Предположим, что ваш первый пример действительно сработал. Затем вы сможете сделать следующее:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // suppose this works
b1.add(new Tree());

Но так как b1 и a1 относятся к одному и тому же объекту, это означает, что a1 теперь относится к List, который содержит как DataNode, так и Tree s. Если вы попытаетесь получить последний элемент, вы получите исключение (не помните, какой из них).

Ответ 10

Хорошо, я буду честен здесь: ленивая реализация в стиле.

Там нет семантической причины, чтобы не допускать вашей первой аффектации.

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