Почему массивы ковариантны, но обобщения инвариантны?
Из книги " Эффективная Java" Джошуа Блоха,
- Массивы отличаются от универсального типа двумя важными способами. Во-первых, массивы ковариантны. Универсальные типы инвариантны.
Ковариантность просто означает, что если X является подтипом Y, то X[] также будет подтипом Y[]. Массивы ковариантны, поскольку строка является подтипом объекта, поэтому
String[] is subtype of Object[]
Инвариантность просто означает, независимо от того, является X подтипом Y или нет ,
List<X> will not be subType of List<Y>.
Мой вопрос в том, почему принято решение сделать массивы ковариантными в Java? Есть и другие сообщения SO, такие как Почему массивы инвариантны, а списки ковариантны?, но они, похоже, сосредоточены на Scala, и я не могу за ними уследить.
Переведено автоматически
Ответ 1
Через википедия:
Ранние версии Java и C # не включали обобщения (они же параметрический полиморфизм).
В таких условиях инвариантность массивов исключает использование полиморфных программ. Например, рассмотрите возможность написания функции для перетасовки массива или функции, которая проверяет два массива на равенство, используя
Object.equals
метод для элементов. Реализация не зависит от точного типа элемента, хранящегося в массиве, поэтому должна быть возможность написать единую функцию, которая работает со всеми типами массивов. Легко реализовать функции типа
boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);Однако, если бы типы массивов рассматривались как инвариантные, было бы возможно вызывать эти функции только для массива точно такого типа
Object[]
. Например, нельзя было бы перетасовать массив строк.Следовательно, и Java, и C # обрабатывают типы массивов ковариантно. Например, в C #
string[]
это подтипobject[]
, а в JavaString[]
это подтипObject[]
.
Это отвечает на вопрос "Почему массивы ковариантны?", или, точнее, "Почему в то время массивы были сделаны ковариантными?"
Когда были введены дженерики, их намеренно не делали ковариантными по причинам, указанным в этом ответе Джона Скита:
Нет, a
List<Dog>
не является aList<Animal>
. Подумайте, что вы можете сделать с aList<Animal>
- вы можете добавить в него любое животное ... включая кошку. Теперь, можете ли вы логически добавить кошку к выводку щенков? Абсолютно нет.
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?Внезапно у вас появился очень сбитый с толку cat.
Первоначальная мотивация для придания массивам ковариантности, описанная в статье в Википедии, неприменима к универсальным, потому что подстановочные знаки делали возможным выражение ковариации (и контравариантности), например:
boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Ответ 2
Причина в том, что каждый массив знает тип своего элемента во время выполнения, в то время как универсальная коллекция - нет из-за стирания типа.
Например:
String[] strings = new String[2];
Object[] objects = strings; // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime
Если бы это было разрешено с универсальными коллекциями:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings; // let's say it is valid
objects.add(12); // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this
Но это вызовет проблемы позже, когда кто-то попытается получить доступ к списку:
String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Ответ 3
Может быть, это поможет:-
Дженерики не ковариантны
Массивы в языке Java ковариантны - это означает, что если Integer расширяет Number (что он и делает), то не только целое число также является числом, но и целое число [] также является Number[]
, и вы вольны передавать или присваивать an, Integer[]
гдеNumber[]
вызывается. (Более формально, если Number является супертипом Integer, то Number[]
является супертипом Integer[]
.) Вы могли бы подумать, что то же самое верно и для универсальных типов - это List<Number>
супертип List<Integer>
, и что вы можете передать a List<Integer>
там, где ожидается a List<Number>
. К сожалению, это так не работает.
Оказывается, есть веская причина, по которой это так не работает: это нарушило бы типобезопасность, которую должны были обеспечивать обобщения. Представьте, что вы могли бы присвоить a List<Integer>
a List<Number>
. Тогда следующий код позволил бы вам поместить что-то, что не является целым числом, в List<Integer>
:
List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));
Поскольку ln - это List<Number>
, добавление к нему значения с плавающей точкой кажется совершенно законным. Но если бы ln было переименовано в li
, то это нарушило бы обещание безопасности типов, подразумеваемое в определении li - что это список целых чисел, поэтому универсальные типы не могут быть ковариантными.
Ответ 4
Важной особенностью параметрических типов является возможность написания полиморфных алгоритмов, то есть алгоритмов, которые работают со структурой данных независимо от значения ее параметра, таких как Arrays.sort()
.
В обобщениях это делается с помощью подстановочных типов:
<E extends Comparable<E>> void sort(E[]);
Чтобы быть действительно полезными, подстановочные типы требуют захвата подстановочных знаков, а для этого требуется понятие параметра типа. Ничего из этого не было доступно на момент добавления массивов в Java, и создание массивов ссылочного типа covariant позволило гораздо проще использовать полиморфные алгоритмы:
void sort(Comparable[]);
Однако такая простота открыла лазейку в системе статических типов:
String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException
требуется проверка во время выполнения каждого доступа на запись к массиву ссылочного типа.
В двух словах, новый подход, воплощенный в дженериках, делает систему типов более сложной, но также и более статически типобезопасной, в то время как старый подход был проще и менее статически типобезопасен. Разработчики языка выбрали более простой подход, имея более важные дела, чем закрытие небольшой лазейки в системе типов, которая редко вызывает проблемы. Позже, когда была создана Java и были решены насущные потребности, у них были ресурсы, чтобы сделать это правильно для обобщений (но изменение этого для массивов привело бы к сбоям в существующих программах Java).