Почему массивы ковариантны, а дженерики инвариантны?

160

От эффективной Явы Джошуа Блох,

  1. Массивы отличаются от универсального типа двумя важными способами. Первые массивы ковариантны. Обобщения являются инвариантами.
  2. Ковариант просто означает, что если X является подтипом Y, то X [] также будет подтипом Y []. Массивы являются ковариантными, так как string является подтипом Object So

    String[] is subtype of Object[]

    Инвариант просто означает, что независимо от того, является ли X подтипом Y или нет,

     List<X> will not be subType of List<Y>.

Мой вопрос, почему решение сделать массивы ковариантными в Java? Существуют и другие сообщения SO, такие как Почему массивы инвариантны, а списки ковариантны? , но они, кажется, сосредоточены на Скале, и я не могу следовать.

жаждующий знаний
источник
1
Разве это не потому, что дженерики были добавлены позже?
Сотириос Делиманолис
1
я думаю, что сравнивать между массивами и коллекциями несправедливо, Коллекции используют массивы в фоновом режиме!
Ахмед Адель Исмаил
4
@ EL-conteDe-monteTereBentikh Не все коллекции, например LinkedList.
Пол Беллора
@PaulBellora Я знаю, что Карты отличаются от реализаторов Коллекции, но я прочитал в SCPJ6, что Коллекции обычно полагаются на массивы !!
Ахмед Адель Исмаил
Потому что нет ArrayStoreException; при вставке неправильного элемента в коллекцию, где он есть в массиве. Таким образом, коллекция может найти это только во время поиска, и это тоже из-за приведения. Таким образом, дженерики обеспечат решение этой проблемы.
Канагавелу Сугамар

Ответы:

151

Через википедию :

Ранние версии Java и C # не включали дженерики (или параметрический полиморфизм).

В таком случае создание инвариантов массивов исключает полезные полиморфные программы. Например, рассмотрите возможность написания функции для перемешивания массива или функции, которая проверяет два массива на равенство, используя Object.equalsметод для элементов. Реализация не зависит от точного типа элемента, хранящегося в массиве, поэтому должна быть возможность написать одну функцию, которая работает со всеми типами массивов. Легко реализовать функции типа

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Однако если бы типы массивов рассматривались как инварианты, эти функции можно было бы вызывать только в массиве точно такого типа Object[]. Нельзя, например, перемешать массив строк.

Поэтому и Java, и C # обрабатывают типы массивов ковариантно. Например, в C # string[]это подтип object[], а в Java String[]это подтип Object[].

Это отвечает на вопрос «Почему массивы ковариантны?» Или, точнее, «Почему массивы были ковариантными в то время

Когда дженерики были введены, они были преднамеренно не сделаны ковариантными по причинам, указанным в этом ответе Джоном Скитом :

Нет, это List<Dog>не так List<Animal>. Подумайте, что вы можете сделать с List<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?

Внезапно у вас очень запутанная кошка.

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

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Пол Беллора
источник
3
да, Arrays допускает полиморфное поведение, однако он вводит исключения во время выполнения (в отличие от исключений времени компиляции с обобщениями). Например:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn
21
Это правильно. Массивы имеют типы reifiable и бросают ArrayStoreExceptions по мере необходимости. Очевидно, что в то время это считалось достойным компромиссом. Сравните это с сегодняшним днем: многие рассматривают ковариацию массива как ошибку, оглядываясь назад.
Пол Беллора
1
Почему «многие» считают это ошибкой? Это гораздо полезнее, чем отсутствие ковариации массива. Как часто вы видели ArrayStoreException; они довольно редки. Ирония здесь непростительна, я думаю ... одна из самых худших ошибок, когда-либо совершенных в Java, - это вариация использования сайта, или подстановочные знаки.
Скотт
3
@ScottMcKinney: «Почему« многие »считают это ошибкой?» AIUI, это потому, что ковариация массива требует динамических тестов типов для всех операций присваивания массива (хотя оптимизация компилятора может помочь?), Что может привести к значительным накладным расходам во время выполнения.
Доминик Деврисе
Спасибо, Доминик, но из моих наблюдений видно, что причина, по которой «многие» считают это ошибкой, заключается в том, что они повторяют то, что сказали другие. Опять же, если взглянуть по-новому на ковариацию массива, это гораздо полезнее, чем повреждение. Опять же, настоящая БОЛЬШАЯ ошибка, допущенная Java, заключалась в универсальной дисперсии сайта с использованием подстановочных знаков. Это вызвало больше проблем, чем я думаю, что многие хотят это признать.
Скотт
30

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

Например:

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
Катона
источник
Я думаю, что ответ Пола Беллора более уместен, поскольку он комментирует ПОЧЕМУ Массивы ковариантны. Если массивы были сделаны инвариантными, то это нормально. у вас будет стирание типа с ним. Основная причина свойства типа Erasure - правильная обратная совместимость?
eagertoLearn
@ user2708477, да, стирание типа было введено из-за обратной совместимости. И да, мой ответ пытается ответить на вопрос в заголовке, почему дженерики инвариантны.
Катона
Тот факт, что массивы знают свой тип, означает, что хотя ковариация позволяет коду запрашивать сохранение чего-либо в массиве, где он не помещается, это не означает, что такое хранилище будет разрешено иметь место. Следовательно, уровень опасности, вызванный ковариантностью массивов, намного меньше, чем если бы они не знали своих типов.
суперкат
@supercat, правильно, я хотел бы отметить, что для дженериков с удалением типов ковариация не могла быть реализована с минимальной безопасностью проверок во время выполнения
Katona
1
Я лично думаю, что этот ответ дает правильное объяснение того, почему массивы ковариантны, когда коллекции не могут быть. Спасибо!
спрашивает
23

Может быть, это поможет: -

Обобщения не ковариантны

Массивы в языке Java являются ковариантными - это означает, что если Integer расширяет Number (что он и делает), то не только Integer также является Number, но Integer [] также является Number[]и вы можете передавать или назначать Integer[]где требуется Number[]. (Более формально, если Number является супертипом типа Integer, то Number[]является супертипом Integer[].) Вы можете подумать, что то же самое верно и для универсальных типов - List<Number>это супертип List<Integer>, и вы можете передать, List<Integer>где 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>, добавление Float к нему кажется совершенно законным. Но если у ln есть псевдоним li, то это нарушит обещание безопасности типов, неявное в определении li, - это список целых чисел, поэтому универсальные типы не могут быть ковариантными.

Рахул Трипати
источник
3
Для массивов вы получаете ArrayStoreExceptionво время выполнения.
Сотириос Делиманолис
4
мой вопрос состоит в WHYтом, что массивы сделаны ковариантными. как упомянул Sotirios, с Arrays можно получить ArrayStoreException во время выполнения, если Arrays были сделаны инвариантными, то мы могли бы обнаружить эту ошибку во время самой компиляции правильно?
eagertoLearn
@eagertoLearn: Одна из основных семантических слабостей Java состоит в том, что в системе типов она ничем не может отличить «Массив, который содержит только производные от него Animal, который не должен принимать никакие элементы, полученные из других источников», от «Массива, который не должен содержать ничего, кроме Animal: и должен быть готов принять предоставленные извне ссылки на Animal. Код, который нуждается в первом, должен принимать массив Cat, а код, который нуждается во втором, не должен. Если компилятор может различать два типа, он может обеспечить проверку во время компиляции. К сожалению, единственное, что их отличает ...
суперкат
... пытается ли код на самом деле что-то сохранить в них, и нет способа узнать это до времени выполнения.
суперкат
3

Массивы ковариантны по крайней мере по двум причинам:

  • Это полезно для коллекций, которые содержат информацию, которая никогда не изменится на ковариантную. Чтобы коллекция T была ковариантной, ее резервное хранилище также должно быть ковариантным. В то время как можно создать неизменную Tколлекцию, которая не использовала бы в T[]качестве своего резервного хранилища (например, используя дерево или связанный список), такая коллекция вряд ли будет работать так же, как и коллекция, поддерживаемая массивом. Кто-то может утверждать, что лучшим способом обеспечить ковариантные неизменяемые коллекции было бы определение типа «ковариантный неизменяемый массив», в котором они могли бы использовать резервное хранилище, но простое использование ковариации массива, вероятно, было проще.

  • Массивы будут часто видоизменяться кодом, который не знает, какой тип объектов будет в них, но не будет помещать в массив то, что не было считано из этого же массива. Ярким примером этого является сортировка кода. Концептуально для типов массива могло бы быть возможным включить методы для обмена или перестановки элементов (такие методы могут быть в равной степени применимы к любому типу массива) или определить объект «манипулятор массива», который содержит ссылку на массив и одну или несколько вещей. он был прочитан из него и может включать методы для хранения ранее прочитанных элементов в массиве, из которого они получены. Если бы массивы не были ковариантными, пользовательский код не смог бы определить такой тип, но среда выполнения могла бы включать некоторые специализированные методы.

Тот факт, что массивы являются ковариантными, может рассматриваться как уродливый хак, но в большинстве случаев это облегчает создание рабочего кода.

Supercat
источник
1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- Хороший вопрос
eagertoLearn
3

Важной особенностью параметрических типов является способность писать полиморфные алгоритмы, то есть алгоритмы, которые работают с структурой данных независимо от значения ее параметра, например Arrays.sort().

С обобщениями это делается с подстановочными типами:

<E extends Comparable<E>> void sort(E[]);

Чтобы быть действительно полезными, подстановочные типы требуют подстановочного знака, а для этого требуется понятие параметра типа. Ничего из этого не было доступно в то время, когда массивы были добавлены в Java, а создание массивов ссылочного типа ковариантно позволило намного проще разрешить полиморфные алгоритмы:

void sort(Comparable[]);

Однако эта простота открыла лазейку в системе статического типа:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

требует проверки во время выполнения каждого доступа на запись в массив ссылочного типа.

Короче говоря, новый подход, воплощенный в дженериках, делает систему типов более сложной, но также более статически безопасной, в то время как более старый подход был проще и менее статически безопасен. Разработчики языка выбрали более простой подход, имея более важные дела, чем закрытие небольшой лазейки в системе типов, которая редко вызывает проблемы. Позже, когда была создана Java и были решены насущные потребности, у них были ресурсы, чтобы сделать это правильно для обобщений (но изменение его для массивов могло бы сломать существующие программы Java).

Meriton
источник
2

Обобщения являются инвариантами : из JSL 4.10 :

... Подтипирование не распространяется на универсальные типы: T <: U не означает, что C<T><: C<U>...

и несколько строк далее, JLS также объясняет, что
массивы ковариантны (первая буква):

4.10.3 Субтипирование среди типов массивов

введите описание изображения здесь

alfasin
источник
2

Я думаю, что они приняли неправильное решение в первую очередь, что сделало массив ковариантным. Это нарушает безопасность типов, как описано здесь, и они застряли с этим из-за обратной совместимости, и после этого они попытались не сделать ту же ошибку для универсального. И это одна из причин, по которой Джошуа Блох предпочитает списки массивам в пункте 25 книги «Эффективная Java (второе издание)»

Арнольд
источник
Джош Блок был автором структуры коллекций Java (1.2) и автором дженериков Java (1.5). Таким образом, парень, который создал дженерики, на которые все жалуются, также совпадал с парнем, который написал книгу, говоря, что они - лучший способ пойти? Не огромный сюрприз!
cpurdy
1

Мое предположение: когда код ожидает массив A [] и вы даете ему B [], где B является подклассом A, есть только две вещи, о которых нужно беспокоиться: что происходит, когда вы читаете элемент массива, и что происходит, если вы пишете Это. Поэтому нетрудно написать языковые правила, чтобы гарантировать, что безопасность типов сохраняется во всех случаях (главное правило состоит в том, что ArrayStoreExceptionможет быть выброшено, если вы попытаетесь вставить A в B []). Тем не менее, для универсального, когда вы объявляете класс SomeClass<T>, Tв теле класса может использоваться любое количество способов , и я предполагаю, что слишком сложно разработать все возможные комбинации, чтобы написать правила о том, когда вещи разрешены, а когда нет.

AJB
источник