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

Из эффективной Java Джошуа Блоха,

  • Массивы отличаются от типичного типа двумя важными способами. Первые массивы ковариантны. Дженерики являются инвариантными.
  • Ковариант просто означает, что если 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, такие как Почему массивы инвариантны, но списки ковариант?, но они, похоже, сосредоточены на Scala, и я не могу следовать.

Ответ 1

Через wikipedia:

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

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

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

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

Следовательно, как Java, так и С# обрабатывают типы массивов ковариантно. Например, в С# string[] есть подтип Object[], а в Java string[] - подтип Object[].

Это отвечает на вопрос "Почему массивы ковариантны?" или, точнее, "Почему в то время были созданы ковариантные массивы?"

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

Нет, a 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?

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

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

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

Похоже, что это не так: это нарушит тип безопасности, который должны были предоставить генераторы. Представьте, что вы можете назначить 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, - что это список целых чисел, поэтому общие типы не могут быть ковариантными.

Ответ 4

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

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

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

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

Ответ 5

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

С дженериками, которые выполняются с помощью подстановочных типов:

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

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

void sort(Comparable[]);

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

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

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

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

Ответ 6

Дженерики являются инвариантными: от JSL 4.10:
p >

... Подтипирование не распространяется на общие типы: T <: U не что C<T> <: C<U>...

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

4.10.3 Подтипирование между типами массивов

enter image description here

Ответ 7

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

Ответ 8

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