Как Java проверяет тип элемента при записи в массив ссылочного типа, если фактический массив создан для другого типа?
Java проверяет совместимость элемента с реальным типом массива, известным во время выполнения. Если ссылка имеет тип массива родителя, но фактический объект является массивом потомка, запись несовместимого объекта завершается ArrayStoreException.
Массивы в Java ковариантны: массив потомка можно присвоить переменной типа массива родителя. Такой подход поддерживает полиморфное использование массивов, но создаёт риск ошибки при записи через более общий тип.
Позднее для обобщённых типов был выбран другой компромисс: обычные параметризованные типы инвариантны. Это позволяет выявлять больше подобных ошибок на этапе компиляции, тогда как ковариантность массивов сохраняет совместимость и удобство работы с массивами.
Переменная может иметь тип массива родителя, хотя фактически ссылается на массив потомка. Компилятор разрешает запись объекта родительского типа через такую переменную, поскольку статический тип ссылки этого не запрещает.
Однако фактический массив не может содержать произвольные экземпляры родительского типа. Если записываемый объект не совместим с реальным компонентным типом массива, программа получает исключение во время выполнения.
У массива есть статический тип переменной и динамический тип объекта. При чтении обычно достаточно статического типа ссылки, но при записи JVM дополнительно проверяет динамический тип самого массива.
Например:
Первая строка допустима из-за ковариантности: Cat[] является подтипом Animal[]. Но во второй строке фактический массив создан как Cat[], поэтому объект Animal, не являющийся Cat, сохранить в него нельзя.
Проверка выполняется во время выполнения, потому что только тогда JVM достоверно знает реальный тип объекта массива. Если записываемый объект совместим с компонентным типом, операция выполняется успешно; например, экземпляр Cat можно записать в этот массив через ссылку Animal[].
Главный компромисс — удобство полиморфизма против потенциальной ошибки времени выполнения. Для коллекций с обобщениями обычно безопаснее использовать List<Animal> или явно ограниченные типы: компилятор чаще обнаруживает несовместимую запись заранее.
Сервис принимает Animal[], потому что ему нужно обрабатывать любых животных. Клиент передаёт туда фактически Cat[], а сервис пытается добавить созданный им объект Animal. В результате ошибка появляется внутри сервиса, хотя причина скрыта в конкретном типе массива, переданном клиентом.
Вариант с сохранением массивов прост и не требует преобразований, но допускает ArrayStoreException. Вариант с List<Animal> обычно безопаснее для добавления элементов, однако меняет API и семантику хранения.
Практичное решение — использовать коллекцию, если метод должен добавлять элементы, либо объявить контракт только для чтения и принимать более общий массив, не выполняя записи. Это снижает зависимость поведения от динамического типа массива и переносит больше проверок на этап компиляции.
Нет, само чтение обычно не вызывает это исключение. JVM проверяет тип при сохранении элемента; при чтении возвращается объект, совместимый со статическим типом компонента массива. Ошибка чтения возможна уже при последующем неверном приведении результата к несовместимому типу.
Потому что он проверяет операцию относительно статического типа переменной. Для Animal[] запись Animal выглядит допустимой, а информация о том, что объект фактически является Cat[], относится к динамическому типу и проверяется JVM во время выполнения.
Нет. Приведение может добавить ещё одну проверку и привести к ClassCastException, если фактический массив не имеет требуемого типа. Оно не меняет сам объект массива и не делает допустимой запись элемента, несовместимого с его реальным компонентным типом.