Чому масиви коваріантні, але генеричні дані інваріантні?


160

З ефективної Java Джошуа Блоха,

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

    String[] is subtype of Object[]

    Інваріант просто означає, незалежно від того, X є підтипом Y чи ні,

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

Моє питання: чому рішення зробити масиви коваріантними на Java? Є й інші повідомлення SO, наприклад, Чому масиви інваріантні, але списки коваріантні? , але вони, схоже, зосереджені на Скалі, і я не в змозі слідувати.


1
Чи не тому, що дженерики додані пізніше?
Sotirios Delimanolis

1
Я думаю, що порівняння між масивами та колекціями несправедливо, колекції використовують масиви у фоновому режимі !!
Ахмед Адель Ісмаїл

4
@ EL-conteDe-monteTereBentikh Не всі колекції, наприклад LinkedList.
Пол Беллора

@PaulBellora Я знаю, що Карти відрізняються від виконавців колекції, але я читав у SCPJ6, що колекції, як правило, покладаються на масиви !!
Ахмед Адель Ісмаїл

Тому що немає ArrayStoreException; при вставленні неправильного елемента в колекцію, де його має масив. Тож колекція може знайти це лише на час пошуку, і це теж через кастинг. Тож дженерики забезпечать вирішення цього питання.
Канагавелу Сугумар

Відповіді:


150

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

Ранні версії 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
Це правильно. Масиви мають типи, що повторюються, і кидайте ArrayStoreExceptions за потребою. Очевидно, що в цей час це вважалося гідним компромісом. На противагу цьому з сьогоднішнім часом: багато хто вважає коваріацію масиву помилкою в ретроспективі.
Пол Беллора

1
Чому "багато" вважають це помилкою? Це набагато корисніше, ніж відсутність коваріації масиву. Як часто ви бачили ArrayStoreException; вони досить рідкісні. Іронія тут непростима, але серед найстрашніших помилок, які коли-небудь робилися на Яві, є варіація використання сайту, яка називається макетів.
Скотт

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

@supercat, правильно, те, що я хотів зазначити, це те, що для дженериків із видаленням типу коваріація не могла бути реалізована з мінімальною безпекою перевірок виконання
Katona

1
Я особисто думаю, що ця відповідь дає правильне пояснення, чому масиви є коваріантними, коли колекції не могли бути. Дякую!
асгс

22

Можливо, це допоможе: -

Генерики не є коваріантними

Масиви на мові Java є коваріантними - це означає, що якщо Integer розширює число (що це робить), то це не тільки Integer також число, але Integer [] також є a Number[], і ви можете вільно пройти або призначити Integer[]де Number[]закликається а. (Більш формально, якщо Number є супертипом Integer, то Number[]це суперпертипом Integer[].) Ви можете подумати, що те саме стосується і загальних типів - List<Number>це супертип List<Integer>, і ви можете передати пропуск, List<Integer>де List<Number>очікується. На жаль, це не працює так.

Виявляється, є вагома причина, що це не працює таким чином: це порушить тип безпеки, який повинні були забезпечити. Уявіть, що ви можете призначити List<Integer>а List<Number>. Тоді наступний код дозволить вам помістити щось таке, що не було цілим числом List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Оскільки ln є a List<Number>, додавання Float до цього видається абсолютно законним. Але якби ln був псевдонім li, то це порушило б обіцянку щодо безпеки типу, що міститься у визначенні li - що це список цілих чисел, і тому загальні типи не можуть бути коваріантними.


3
За масиви ви отримуєте ArrayStoreException виконання.
Сотіріос Деліманоліс

4
моє питання - WHYце масиви коваріантні. як згадував Sotirios, з Arrays можна було б отримати ArrayStoreException під час виконання, якби масиви були інваріантними, то ми могли б виявити цю помилку під час компіляції правильно?
eagertoLearn

@eagertoLearn: Однією з основних семантичних слабкостей Java є те, що ніщо у своїй системі типів не може відрізнити "масив, який не містить нічого, крім похідних Animal, який не повинен приймати будь-які елементи, отримані з інших місць", від "масиву, який не повинен містити нічого, окрім Animal, і повинен бути готовий приймати посилання, що надаються зовні Animal. Код, який потребує першого, повинен приймати масив Cat, але код, який потребує останній, не повинен. Якщо компілятор міг би виділити два типи, він може забезпечити перевірку часу компіляції. На жаль, єдине, що їх відрізняє ...
supercat

... це чи код намагається насправді щось зберігати в них, і немає способу знати це до часу виконання.
supercat

3

Масиви є коваріантними щонайменше з двох причин:

  • Це корисно для колекцій, які містять інформацію, яка ніколи не зміниться на коваріантну. Щоб колекція T була коваріантною, її зберігання також має бути коваріантним. Хоча можна було б створити незмінну Tколекцію, яка не використовувала б T[]його сховище резервного копіювання (наприклад, використовуючи дерево або зв'язаний список), така колекція навряд чи зможе виконати так само, як та, яку підтримує масив. Можна стверджувати, що кращим способом забезпечення коваріантних незмінних колекцій було б визначення типу "коваріантного незмінного масиву", яким вони могли б користуватися резервним сховищем, але просто дозволити коваріацію масиву було, мабуть, простіше.

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

Той факт, що масиви є коваріантними, може розглядатися як некрасивий злом, але в більшості випадків це полегшує створення робочого коду.


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).


2

Дженріки інваріантні : від JSL 4.10 :

... Підтипізація не поширюється на загальні типи: T <: U не означає, що C<T><: C<U>...

і кілька рядків далі, JLS також пояснює, що
масиви є коваріантними (перша куля):

4.10.3 Підтипи серед типів масивів

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


2

Я думаю, що вони прийняли неправильне рішення в першу чергу, що зробило масив коваріантом. Це порушує безпеку типу, як описано тут, і вони зациклювались на цьому через відсталість сумісності, а потім намагалися не робити тієї самої помилки для загальної. І це одна з причин того, що Джошуа Блох вважає за краще перелічити аргументи в пункті 25 книги "Ефективна Java (друге видання)"


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

1

Моє взяття: Коли код очікує масив A [], і ви даєте йому B [], де B є підкласом A, турбуватися можна лише про дві речі: що відбувається, коли ви читаєте елемент масиву, і що станеться, якщо ви пишете це. Тому не важко написати мовні правила, щоб забезпечити збереження безпеки типу у всіх випадках (головне правило полягає в тому, що знак ArrayStoreExceptionможе бути кинутий, якщо ви спробуєте вставити A в B []). Однак для загального, коли ви оголошуєте клас SomeClass<T>, Tв тілі класу може бути використана будь-яка кількість способів , і я здогадуюсь, що це надто складно, щоб виробити всі можливі комбінації, щоб написати правила про те, коли речі дозволені, а коли їх немає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.