1. Обзор
В этом кратком руководстве мы сосредоточимся на ClassCastException
, распространенном исключении Java .
ClassCastException
— это непроверенное исключение , которое сигнализирует о том, что код попытался привести ссылку к типу, подтипом которого он не является .
Давайте рассмотрим некоторые сценарии, которые приводят к возникновению этого исключения, и то, как мы можем их избежать.
2. Явный кастинг
Для наших следующих экспериментов рассмотрим следующие классы:
public interface Animal {
String getName();
}
public class Mammal implements Animal {
@Override
public String getName() {
return "Mammal";
}
}
public class Amphibian implements Animal {
@Override
public String getName() {
return "Amphibian";
}
}
public class Frog extends Amphibian {
@Override
public String getName() {
return super.getName() + ": Frog";
}
}
2.1. Классы кастинга
На сегодняшний день наиболее распространенным сценарием возникновения ClassCastException
является явное приведение к несовместимому типу.
Например, давайте попробуем привести лягушку
к млекопитающему
:
Frog frog = new Frog();
Mammal mammal = (Mammal) frog;
Мы могли бы ожидать здесь ClassCastException
, но на самом деле мы получаем ошибку компиляции: «несовместимые типы: лягушка не может быть преобразована в млекопитающее». Однако ситуация меняется, когда мы используем общий супертип:
Animal animal = new Frog();
Mammal mammal = (Mammal) animal;
Теперь мы получаем ClassCastException
из второй строки:
Exception in thread "main" java.lang.ClassCastException: class Frog cannot be cast to class Mammal (Frog and Mammal are in unnamed module of loader 'app')
at Main.main(Main.java:9)
Проверенное нисходящее приведение к Mammal
несовместимо со ссылкой Frog, потому что Frog
не
является подтипом Mammal
. В этом случае компилятор нам помочь не сможет, так как переменная Animal
может содержать ссылку совместимого типа.
Интересно отметить, что ошибка компиляции возникает только тогда, когда мы пытаемся выполнить приведение к однозначно несовместимому классу. То же самое не верно для интерфейсов, потому что Java поддерживает множественное наследование интерфейсов, но только одиночное наследование для классов. Таким образом, компилятор не может определить, реализует ли ссылочный тип конкретный интерфейс или нет. Приведем пример:
Animal animal = new Frog();
Serializable serial = (Serializable) animal;
Мы получаем ClassCastException
во второй строке вместо ошибки компиляции:
Exception in thread "main" java.lang.ClassCastException: class Frog cannot be cast to class java.io.Serializable (Frog is in unnamed module of loader 'app'; java.io.Serializable is in module java.base of loader 'bootstrap')
at Main.main(Main.java:11)
2.2. Приведение массивов
Мы видели, как классы обрабатывают приведение типов, теперь давайте посмотрим на массивы. Приведение массивов работает так же, как приведение классов. Однако нас может запутать автоупаковка и продвижение шрифта или их отсутствие.
Итак, давайте посмотрим, что происходит с примитивными массивами, когда мы пытаемся выполнить следующее приведение:
Object primitives = new int[1];
Integer[] integers = (Integer[]) primitives;
Вторая строка генерирует исключение ClassCastException
, так как автоупаковка не работает для массивов.
Как насчет продвижения типа? Давайте попробуем следующее:
Object primitives = new int[1];
long[] longs = (long[]) primitives;
Мы также получаем ClassCastException
, потому что повышение типа не работает для целых массивов.
2.3. Безопасный кастинг
В случае явного приведения настоятельно рекомендуется проверить совместимость типов перед попыткой приведения с использованием instanceof
.
Давайте посмотрим на пример безопасного приведения:
Mammal mammal;
if (animal instanceof Mammal) {
mammal = (Mammal) animal;
} else {
// handle exceptional case
}
3. Загрязнение кучи
В соответствии со спецификацией Java : « Загрязнение кучи может произойти только в том случае, если программа выполнила какую-либо операцию с необработанным типом, которая привела бы к непроверенному предупреждению во время компиляции».
Для нашего эксперимента рассмотрим следующий универсальный класс:
public static class Box<T> {
private T content;
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
Теперь попробуем загрязнить кучу следующим образом:
Box<Long> originalBox = new Box<>();
Box raw = originalBox;
raw.setContent(2.5);
Box<Long> bound = (Box<Long>) raw;
Long content = bound.getContent();
Последняя строка вызовет исключение ClassCastException
, поскольку она не может преобразовать двойную
ссылку в Long
.
4. Общие типы
При использовании дженериков в Java мы должны опасаться стирания типов , что также может привести к ClassCastException
в некоторых условиях.
Рассмотрим следующий общий метод:
public static <T> T convertInstanceOfObject(Object o) {
try {
return (T) o;
} catch (ClassCastException e) {
return null;
}
}
А теперь назовем это:
String shouldBeNull = convertInstanceOfObject(123);
На первый взгляд, мы можем разумно ожидать, что из блока catch будет возвращена нулевая ссылка. Однако во время выполнения из-за стирания типа параметр приводится к Object
вместо String
. Таким образом, перед компилятором стоит задача присвоить Integer String
,
что вызовет исключение ClassCastException.
5. Вывод
В этой статье мы рассмотрели ряд распространенных сценариев неуместного кастинга.
Неявное или явное приведение ссылок Java к другому типу может привести к ClassCastException
, если только целевой тип не является тем же или потомком фактического типа .
Код, использованный в этой статье, можно найти на GitHub .