Массивы ковариантны
Говорят, что массивы ковариантны, что в основном означает, что, учитывая правила подтипирования Java, массив типа T[]
может содержать элементы типа T
или любого подтипа T
. Например
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Но не только это, правила подтипирования Java также заявляют, что массив S[]
является подтипом массива, T[]
если S
является подтипом T
, поэтому, что-то вроде этого также допустимо:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Поскольку согласно правилам подтипирования в Java, массив Integer[]
является подтипом массиваNumber[]
потому что Integer является подтипом числа.
Но это правило подтипов может привести к интересному вопросу: что произойдет, если мы попытаемся это сделать?
myNumber[0] = 3.14; //attempt of heap pollution
Эта последняя строка будет хорошо скомпилирована, но если мы запустим этот код, мы получим ArrayStoreException
потому что мы пытаемся поместить double в целочисленный массив. Тот факт, что мы обращаемся к массиву через ссылку Number, здесь не имеет значения, важно то, что массив является массивом целых чисел.
Это означает, что мы можем обмануть компилятор, но мы не можем обмануть систему типов во время выполнения. И это так, потому что массивы - это то, что мы называем типом reifiable. Это означает, что во время выполнения Java знает, что этот массив был фактически создан как массив целых чисел, к которым просто случается обращение через ссылку типа Number[]
.
Итак, как мы видим, одна вещь - это фактический тип объекта, другая вещь - это тип ссылки, которую мы используем для доступа к ней, верно?
Проблема с Java Generics
Теперь проблема с универсальными типами в Java заключается в том, что информация о типе для параметров типа отбрасывается компилятором после завершения компиляции кода; поэтому информация этого типа недоступна во время выполнения. Этот процесс называется стиранием типа . Существуют веские причины для реализации таких обобщений в Java, но это длинная история, и она связана с бинарной совместимостью с уже существующим кодом.
Важным моментом здесь является то, что, поскольку во время выполнения нет информации о типе, нет способа гарантировать, что мы не допустим загрязнения кучи.
Рассмотрим теперь следующий небезопасный код:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Если компилятор Java не мешает нам делать это, система типов во время выполнения также не может остановить нас, потому что во время выполнения нет никакого способа определить, что этот список должен быть только списком целых чисел. Среда выполнения Java позволила бы нам помещать все, что мы хотим в этот список, когда он должен содержать только целые числа, потому что, когда он был создан, он был объявлен как список целых чисел. Вот почему компилятор отклоняет строку № 4, потому что это небезопасно и, если разрешено, может нарушить предположения системы типов.
Поэтому разработчики Java позаботились о том, чтобы мы не могли обмануть компилятор. Если мы не можем обмануть компилятор (как мы можем сделать с массивами), то мы не можем обмануть и систему типов во время выполнения.
Таким образом, мы говорим, что универсальные типы не подлежат повторному определению, поскольку во время выполнения мы не можем определить истинную природу универсального типа.
Я пропустил некоторые части этих ответов, вы можете прочитать полную статью здесь:
https://dzone.com/articles/covariance-and-contravariance