Подтвердить что ты не робот

Как массивы "запоминают" их типы на Java?

Рассмотрим следующий код:

class AA { }

class BB extends AA { }

public class Testing {

    public static void main(String[] args) {
        BB[] arr = new BB[10];
        AA[] arr2 = arr;

        BB b = new BB();
        AA a = new AA();
        arr2[0] = a; // ArrayStoreException at runtime
        arr2[1] = b;

        List<BB> listBB = new ArrayList<>();
        List listAA = listBB;
        listAA.add("hello world.txt");

    }
}

В приведенном выше примере я получаю ArrayStoreException при попытке arr2[0] = a. Это означает, что массив запоминает, какой тип он должен принять. Но List не помнит их. Он просто компилируется и работает нормально. ClassCastException будет сброшен, когда я извлечу объект BB.

Итак, вопросы:

  • Как массив запоминает его тип (я знаю, что он называется "reification" ). Как это происходит точно?

  • И почему только массивы снабжаются этой мощью, но не ArrayList, хотя он использует массив под капотом.

  • Почему во время компиляции не может быть обнаружено ArrayStoreException, т.е. когда я делаю arr2[0] = a, это может вызвать ошибку компилятора, а не обнаруживать его во время выполнения.

Спасибо.

4b9b3361

Ответ 1

  • Информация о типе массивов, в отличие от дженериков, хранится во время выполнения. Это было частью Java с самого начала. Во время выполнения a AA[] можно отличить от BB[], потому что JVM знает их типы.

  • An ArrayList (и остальная часть структуры Collections) использует дженерики, которые подвержены стиранию типа. Во время выполнения параметр универсального типа недоступен, поэтому ArrayList<BB> неотличим от ArrayList<AA>; они оба просто ArrayList для JVM.

  • Компилятор знает только, что arr2 является AA[]. Если у вас есть AA[], компилятор может только предположить, что он может хранить AA. Компилятор не обнаружит проблему безопасности типа в том, что вы размещаете AA в том, что действительно есть BB[], потому что оно видит только ссылку AA[]. В отличие от generics, массивы Java являются ковариантными, поскольку BB[] является AA[], потому что a BB является AA. Но это указывает на возможность того, что вы только что продемонстрировали - ArrayStoreException, потому что объект, на который ссылается arr2, действительно является BB[], который не будет обрабатывать AA как элемент.

Ответ 2

1. Каждый раз, когда значение хранится в массиве, компилятор вставляет чек. Затем во время выполнения он проверяет, что тип значения равен типу времени выполнения массива.

2. Были введены дженерики. Дженерики являются инвариантными и могут быть проверены во время компиляции. (Во время выполнения общие типы стираются).

3. Вот пример неудачного случая (из википедии):

// a is a single-element array of String
String[] a = new String[1];

// b is an array of Object
Object[] b = a;

// Assign an Integer to b. This would be possible if b really were
// an array of Object, but since it really is an array of String,
// we will get a java.lang.ArrayStoreException.
b[0] = 1;

Компилятор не может обнаружить, что третий оператор приведет к ArrayStoreException. Что касается третьего утверждения, компилятор видит, что мы добавляем Integer в массив Object []. Это совершенно законно.

Фон/Рассуждение (из wikipedia)

В ранних версиях Java и С# не было дженериков (параметрический полиморфизм a.k.a.). В такой настройке, делая инварианты массивов, исключают полезные полиморфные программы. Например, рассмотрим возможность записи функции для перетасовки массива или функции, которая проверяет два массива на равенство, используя метод Object.equals для элементов. Реализация не зависит от точного типа элемента, хранящегося в массиве, поэтому должно быть возможно написать одну функцию, которая работает на всех типах массивов. Легко реализовать функции типа

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Однако, если типы массивов рассматривались как инвариантные, их можно было бы вызвать только в массиве точно типа Object []. Например, нельзя было перетасовать массив строк.

Следовательно, как Java, так и С# обрабатывают типы массивов ковариантно. Например, в С# строка [] является подтипом объекта [], а в Java String [] является подтипом Object []

Ответ 3

Массивы явно запоминают свой тип, вы даже можете получить его во время выполнения (array.getClass(). getComponentType()).

При хранении в массиве виртуальная машина проверяет, соответствует ли элемент, который будет храниться, совместимым с типом элемента массива. Если это не так, вы получите ArrayStoreException.

Коллекции (как ArrayList) внутренне объявляют свои базовые массивы как Object [], поэтому они могут хранить что угодно, даже типы, которые им не разрешено хранить по их общему определению.

Ответ 4

3) Выполняете ли вы AA a = new AA(); или AA a = new BB();, компилятор не помнит позже то, что вы назначили a, только его объявленный тип AA. Однако в последнем случае вы можете фактически присвоить значение a элементу a BB[], поэтому arr2[0] = a; не должно давать вам исключение во время выполнения. Таким образом, компилятор не может сказать заранее. (Кроме того, вы можете попробовать неприятные вещи, чтобы изменить значение a во время выполнения между соответствующими строками...)

2) Если бы вы использовали List<AA> listAA = listBB;, вы получили бы ошибку компиляции. Итак, что вы ожидали от примера массива - определение времени компиляции возникающего невозможного назначения - на самом деле работает со списками! Однако, если вы не укажете параметр родового типа, вы получите список необработанных типов, которому вы можете назначить другие списки без разумных проверок типа. Это лучше всего считать оставшимся от ранней Java, чего следует избегать. Если вы добавили следующую строку ниже кода вопроса:

BB item = listBB.get(0);

Как вы думаете, он должен/будет компилироваться? Должно ли/будет выполняться (и, если да, каким должен быть его результат)?

Как часть 1), вероятно, требует отдельного вопроса.

Ответ 5

  • Проще говоря: массивы - это класс. BB[] не распространяется AA[], хотя BB расширяет AA.
  • В первые дни преддипики. ArrayList Объекты. поэтому все может быть проведено в ArrayList. Если у вас есть массив из Object, вы можете играть в ту же игру. Теперь с помощью дженериков вы можете указать ArrayList с определенным типом типа ArrayList<String>.
  • на самом деле проблема в строке 2:

    AA[] arr2=arr;
    

Пусть проблема становится немного проще. У вас есть класс автомобиля, велосипед и класс автомобиля, который расширяет автомобиль. У вас не может быть

Bike b=new Bike()
Car c=b;

arr2 - массив, а arr - массив, но класс отличается. Помогает ли это?