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

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

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

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

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

Animal[] x=new Dog[3];
4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Проблема с Java Generics

Теперь проблема с типичными типами Java заключается в том, что информация о типе отбрасывается компилятором, и она недоступна во время выполнения. Этот процесс называется type erasure. Есть хорошая причина для реализации таких обобщений в 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 runtime позволит вам помещать все, что вы хотите в этот список, когда он должен содержать только целые числа, потому что когда он был создан, он был объявлен как список целых чисел.

Таким образом, разработчики 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); 

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

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

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

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

контрвариация

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

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

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

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

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

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

Принцип Get/Put

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

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

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

Ответ 2

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

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

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

Prentice Hall - Эффективное Java 2nd Edition

Ответ 3

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

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

Ответ 4

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

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

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

Массивы и дженерики не смешиваются.

Ответ 5

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>();

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

private void addCat(List<? extends Animal> animals) {
    animals.add(null);      // it ok
    animals.add(new Cat()); // compilation error here
}

Ответ 6

Окончательный ответ заключается в том, что именно так 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 имеет сложное мета-требование обратной совместимости; то есть расширение языка не должно нарушать старый код. Между прочим, это означает, что невозможно зафиксировать исторические ошибки, например, как работает массив. (Предполагая, что это признано ошибкой...)


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

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

Ответ 7

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