То, что вы видите во втором случае - это ковариация массива. Это плохая вещь IMO, которая делает присваивания внутри массива небезопасными - они могут не сработать во время выполнения, несмотря на то, что во время компиляции все было в порядке.
В первом случае, представьте, что код did скомпилировался, и за ним последовало:
b1.add(new SomeOtherTree());
DataNode node = a1.get(0);
Что вы ожидаете произойти?
Вы можете сделать это:
List<DataNode> a1 = new ArrayList<DataNode>();
List<? extends Tree> b1 = a1;
... потому что тогда вы можете брать вещи только из b1
, и они гарантированно будут совместимы с Tree
. Вы не можете вызвать b1.add(...)
именно потому, что компилятор не будет знать, безопасно это или нет.
Посмотрите этот раздел FAQ по Java Generics от Angelika Langer для получения дополнительной информации.
Ну, здесь я буду честен: ленивая реализация genericity.
Нет никаких семантических причин не разрешать ваш первый аффект.
Кстати, хотя я обожал шаблонизацию в C++, дженерики, вместе с таким глупым ограничением, которое мы имеем здесь, являются основной причиной, почему я отказался от Java.
Краткое объяснение: было ошибкой разрешить это изначально для массивов.
Более длинное объяснение:
Предположим, что это разрешено:
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's stuff in a1 that isn't a DataNode!!
}
Идеальное решение разрешило бы нужный вам вид приведения при использовании варианта List
, доступного только для чтения, но запретило бы его при использовании интерфейса (например, List
), доступного для чтения и записи. Java не допускает такого рода обозначения дисперсии для параметров generics, (*) но даже если бы допускала, вы не смогли бы привести List
к List
, если A
и B
не идентичны.
(*) То есть не допускает этого при написании классов. Вы можете объявить свою переменную как тип List extends Tree>
, и это нормально.
Когда массивы были разработаны (т.е. практически когда была разработана java), разработчики решили, что дисперсия будет полезна, и разрешили ее. Однако это решение часто критиковали, потому что оно позволяет делать следующее (предположим, что NotADataNode
является другим подклассом Tree
):
DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2; // this is okay
b2[0] = new NotADataNode(); //compiles fine, causes runtime error
Поэтому, когда были разработаны дженерики, было решено, что общие структуры данных должны допускать только явную дисперсию. Т.е. вы не можете сделать List
, но можете сделать List extends Tree> b1 = a1;
.
Однако если вы сделаете последнее, попытка использовать метод add
или set
(или любой другой метод, принимающий в качестве аргумента T
) вызовет ошибку компиляции. Таким образом, невозможно заставить компилироваться эквивалент приведенной выше проблемы с массивом (без небезопасного приведения).
List
не расширяет List
, хотя 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
Вот почему, когда они добавили обобщения в Коллекции, они решили сделать это немного по-другому, чтобы предотвратить ошибки времени выполнения.
Краткий ответ: Список a1 не того же типа, что и список b2; В a1 вы можете поместить любой объектный тип, который имеет расширение DataNode. Таким образом, он может содержать другие типы, кроме Tree.
Это ответ из C#, но я думаю, что здесь это не имеет значения, так как причина та же.
"В частности, в отличие от типов массивов, построенные ссылочные типы не проявляют "ковариантных" преобразований. Это означает, что тип List не имеет преобразования (ни неявного, ни явного) к List, даже если B является производным от A. Аналогично, не существует преобразования от List к List
Причина этого проста: если преобразование в List разрешено, то, очевидно, можно хранить в списке значения типа A. Но это нарушит инвариантность. Но это нарушит инвариант, согласно которому каждый объект в списке типа List всегда является значением типа B, иначе могут возникнуть непредвиденные сбои при присваивании в классах коллекций."
http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e
DataNode может быть подтипом Tree, но List DataNode не является подтипом List Tree.
https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html
Это классическая проблема с универсальными шаблонами, реализованными со стиранием типа.
Предположим, что ваш первый пример действительно сработал. Тогда вы сможете сделать следующее:
List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1; // suppose this works
b1.add(new Tree());
Но поскольку b1
и a1
относятся к одному и тому же объекту, это означает, что a1
теперь относится к Список
, содержащий как DataNode
, так и Tree
. Если вы попытаетесь получить этот последний элемент, вы получите исключение (не помню, какой именно).