Массивы ковариантны
Массивы называются ковариантными, что в основном означает, что, учитывая правила подтипирования 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
Теперь проблема с универсальными типами в 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