почему полиморфизм не обрабатывает общие коллекции и простые массивы одинаково?

предположим, что класс Dog расширяет класс Animal: почему этот полиморфный оператор не разрешен:

List<Animal> myList = new ArrayList<Dog>();

Однако это разрешено с простыми массивами:

Animal[] x=new Dog[3];

person Java Player    schedule 26.05.2012    source источник
comment
Есть те, кто возразит, что позволять массивам делать это было плохой идеей.   -  person Jeffrey    schedule 27.05.2012
comment
Причиной являются стирание типов и дженерики, а не коллекции.   -  person duffymo    schedule 27.05.2012
comment
Короткий ответ: общие контейнеры не являются массивами. Более длинный ответ, как предложил duffymo alreyad, — стирания: code.stephenmorley.org /articles/java-generics-type-erasure   -  person paulsm4    schedule 27.05.2012
comment
@paulsm4 спасибо за ссылку. Это интересное чтение.   -  person toniedzwiedz    schedule 27.05.2012
comment
@duffymo - стирание типа связано с этим, но не объясняет это (IMO). Смотрите мой ответ.   -  person Stephen C    schedule 27.05.2012
comment
@duffymo: При чем тут стирание? Нековариантность изменяемых последовательностей является желательной функцией, а не ошибкой (и проверка типа происходит до того, как информация о типе будет стерта).   -  person Niklas B.    schedule 29.05.2012


Ответы (7)


Причины этого основаны на том, как Java реализует дженерики.

Пример массивов

С массивами вы можете сделать это (массивы ковариантны, как объяснили другие)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

Но что произойдет, если вы попытаетесь это сделать?

Number[0] = 3.14; //attempt of heap pollution

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

Это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типов времени выполнения. И это так, потому что массивы — это то, что мы называем повторяемыми типами. Это означает, что во время выполнения Java знает, что этот массив фактически создан как массив целых чисел, доступ к которому просто происходит через ссылку типа Number[].

Итак, как видите, одно дело — фактический тип объекта, другое — тип ссылки, которую вы используете для доступа к нему, верно?

Проблема с Java Generics

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

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

Например,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

Если компилятор Java не остановит вас от этого, система типов времени выполнения также не сможет вас остановить, потому что во время выполнения нет способа определить, что этот список должен быть списком только целых чисел. Среда выполнения Java позволит вам поместить в этот список все, что вы хотите, хотя он должен содержать только целые числа, потому что при создании он был объявлен как список целых чисел.

Таким образом, разработчики Java позаботились о том, чтобы компилятор невозможно было обмануть. Если вы не можете обмануть компилятор (как мы можем сделать с массивами), вы также не можете обмануть систему типов времени выполнения.

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

Очевидно, это затруднило бы полиморфизм. Рассмотрим следующий пример:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Теперь вы можете использовать его так:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

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

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Вы получите ошибки компилятора, если попытаетесь...

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

Решение состоит в том, чтобы научиться использовать две мощные функции дженериков Java, известные как ковариантность и контравариантность.

Ковариация

С помощью ковариации вы можете читать элементы из структуры, но вы не можете ничего в нее записывать. Все это действительные объявления.

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()

И вы можете прочитать из myNums:

Number n = myNums.get(0); 

Потому что вы можете быть уверены, что все, что содержит фактический список, может быть преобразовано в число (в конце концов, все, что расширяет число, является числом, верно?)

Однако вам не разрешено помещать что-либо в ковариантную структуру.

myNumst.add(45L); //compiler error

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

Контравариантность

С контравариантностью можно сделать наоборот. Вы можете поместить вещи в общую структуру, но вы не можете прочитать ее.

List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

В этом случае фактическая природа объекта — это список объектов, и благодаря контравариантности вы можете поместить в него числа, в основном потому, что все числа имеют объект как своего общего предка. Таким образом, все Числа являются объектами, и, следовательно, это верно.

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

Number myNum = myNums.get(0); //compiler-error

Как видите, если бы компилятор разрешил вам написать эту строку, вы бы получили исключение ClassCastException во время выполнения.

Принцип получения/вложения

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

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

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

Благодаря силам ковариантности и контравариантности это работает для такого случая:

List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);
person Edwin Dalorzo    schedule 30.05.2012

Массивы отличаются от универсальных типов двумя важными способами. Во-первых, массивы ковариантны. Это пугающе звучащее слово просто означает, что если Sub является подтипом Super, то тип массива Sub[] является подтипом Super[]. Дженерики, напротив, инвариантны: для любых двух различных типов Type1 и Type2 List‹Type1› не является ни подтипом, ни надтипом List‹Type2›.

[..]Второе важное различие между массивами и дженериками заключается в том, что массивы овеществлены [JLS, 4.7]. Это означает, что массивы знают и применяют свои типы элементов во время выполнения.

[..] Обобщения, напротив, реализуются путем стирания [JLS, 4.6]. Это означает, что они применяют свои ограничения типа только во время компиляции и отбрасывают (или стирают) информацию о своем типе элемента во время выполнения. Стирание — это то, что позволяет универсальным типам свободно взаимодействовать с унаследованным кодом, не использующим универсальные типы (статья 23). Из-за этих фундаментальных различий массивы и дженерики плохо сочетаются друг с другом. Например, создание массива универсального типа, параметризованного типа или параметра типа является незаконным. Ни одно из этих выражений создания массива не является допустимым: новый список‹E›[], новый список‹String›[], новый E[]. Все это приведет к общим ошибкам создания массива во время компиляции.[..]

Prentice Hall — Эффективная версия Java, 2-е издание

person user278064    schedule 26.05.2012

Это очень интересно. Я не могу сказать вам ответ, но это работает, если вы хотите поместить список собак в список животных:

List<Animal> myList = new ArrayList<Animal>();
myList.addAll(new ArrayList<Dog>());
person Community    schedule 26.05.2012
comment
Это работает, потому что addAll() перебирает переданную коллекцию и вызывает List.add() для каждого элемента в коллекции. И List<Animal> абсолютно может иметь элементы, которые являются любым подклассом Animal. - person QuantumMechanic; 27.05.2012
comment
@QuantumMechanic Да, я знал, почему это работает, я просто опубликовал это на случай, если он застрял на том, что ему нужен обходной путь. - person ; 27.05.2012

Способ кодирования версии коллекций, чтобы она компилировалась:

List<? extends Animal> myList = new ArrayList<Dog>();

Причина, по которой вам это не нужно с массивами, связана с стиранием типа - массивы не-примитивов - это всего лишь Object[], а массивы java не являются типизированным классом (как коллекции). Язык никогда не был предназначен для этого.

Массивы и дженерики несовместимы.

person Bohemian♦    schedule 26.05.2012
comment
Для меня это звучит наоборот, но, возможно, я неправильно истолковываю то, что вы говорите. Вы говорите, что все массивы не-примитивов - это просто Object[], а массивы java не являются типизированным классом (как коллекции), но разве это не наоборот? Массивы Java сохраняют свой тип во время выполнения, а коллекции — нет из-за стирания типа. - person Alexis King; 27.05.2012

List<Animal> myList = new ArrayList<Dog>();

невозможно, потому что в этом случае вы могли бы поместить кошек в собак:

private void example() {
    List<Animal> dogs = new ArrayList<Dog>();
    addCat(dogs);
    // oops, cat in dogs here
}

private void addCat(List<Animal> animals) {
    animals.add(new Cat());
}

С другой стороны

List<? extends Animal> myList = new ArrayList<Dog>();

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

private void addCat(List<? extends Animal> animals) {
    animals.add(null);      // it's ok
    animals.add(new Cat()); // compilation error here
}
person Peter Bagyinszki    schedule 27.05.2012

Окончательный ответ таков, потому что Java был указан таким образом. Точнее, потому что именно так развивалась спецификация Java *.

Мы не можем сказать, как на самом деле думали дизайнеры Java, но подумайте вот о чем:

List<Animal> myList = new ArrayList<Dog>();
myList.add(new Cat());   // compilation error

против

Animal[] x = new Dog[3];
x[0] = new Cat();        // runtime error

Здесь будет выдана ошибка времени выполнения: ArrayStoreException. Потенциально это может быть вызвано любым присвоением любому массиву не-примитивов.

Можно сделать вывод, что обработка типов массивов в Java неверна... из-за примеров, подобных приведенному выше.

* Обратите внимание, что типизация массивов Java была определена до Java 1.0, но универсальные типы были добавлены только в Java 1.5. Язык Java имеет общее мета-требование обратной совместимости; т. е. языковые расширения не должны нарушать старый код. Помимо прочего, это означает, что невозможно исправить исторические ошибки, например, в том, как работает типизация массива. (Если предположить, что было ошибкой...)


Что касается универсального типа, стирание типа не объясняет ошибку компиляции. Ошибка компиляции на самом деле возникает из-за проверки типа компиляции с использованием нестертых универсальных типов.

И на самом деле, вы можете подорвать ошибку компиляции, используя приведение типа uncheck (игнорировать предупреждение), и в конечном итоге оказаться в ситуации, когда ваш ArrayList<Dog> фактически содержит Cat объектов во время выполнения. (Это является следствием стирания типов!) Но имейте в виду, что ваша подмена ошибок компиляции с использованием непроверенного преобразования может привести к ошибкам времени выполнения в неожиданных местах... если вы ошибетесь. Вот почему это плохая идея.

person Stephen C    schedule 27.05.2012

До появления дженериков написание подпрограммы, которая могла бы сортировать массивы произвольного типа, требовало либо возможности (1) создавать массивы только для чтения ковариантным образом и менять местами или переупорядочивать элементы независимо от типа, либо (2) создавать массивы для чтения и записи ковариантным образом, которые можно безопасно читать и можно безопасно записывать с теми элементами, которые ранее были прочитаны из того же массива, или (3) иметь массивы, предоставляющие некоторые независимые от типа средства сравнения элементов. Если бы ковариантные и контравариантные универсальные интерфейсы были включены в язык с самого начала, первый подход мог бы быть лучшим, поскольку он позволил бы избежать необходимости выполнять проверку типов во время выполнения, а также возможность того, что такие проверки типов может потерпеть неудачу. Тем не менее, поскольку такой универсальной поддержки не существовало, не было ничего, к чему можно было бы разумно привести массив производного типа, кроме массива базового типа.

person supercat    schedule 28.05.2012