«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. Приведение классов

Frog frog = new Frog();
Mammal mammal = (Mammal) frog;

На сегодняшний день наиболее распространенным сценарием возникновения ClassCastException является явное приведение к несовместимому типу.

Animal animal = new Frog();
Mammal mammal = (Mammal) animal;

Например, давайте попробуем привести лягушку к млекопитающему:

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)

Мы могли бы ожидать здесь ClassCastException, но на самом деле мы получаем ошибку компиляции: «несовместимые типы: лягушка не может быть преобразована в млекопитающее» € . Однако ситуация меняется, когда мы используем общий супертип:

Теперь мы получаем ClassCastException во второй строке: не является подвидом млекопитающих. В этом случае компилятор нам помочь не сможет, так как переменная Animal может содержать ссылку совместимого типа.

Animal animal = new Frog();
Serializable serial = (Serializable) animal;

Интересно отметить, что ошибка компиляции возникает только тогда, когда мы пытаемся выполнить приведение к однозначно несовместимому классу. То же самое не верно для интерфейсов, потому что Java поддерживает множественное наследование интерфейсов, но только одиночное наследование для классов. Таким образом, компилятор не может определить, реализует ли ссылочный тип конкретный интерфейс или нет. Приведем пример:

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)

Мы получаем ClassCastException во второй строке вместо ошибки компиляции:

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, поскольку она не может преобразовать Double ссылка на Лонг.

4. Универсальные типы

При использовании универсальных типов в Java мы должны опасаться стирания типов, что также может привести к ClassCastException в некоторых условиях.

public static <T> T convertInstanceOfObject(Object o) {
    try {
        return (T) o;
    } catch (ClassCastException e) {
        return null;
    }
}

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

String shouldBeNull = convertInstanceOfObject(123);

А теперь назовем его:

На первый взгляд, мы можем разумно ожидать, что из блока catch будет возвращена нулевая ссылка. Однако во время выполнения из-за стирания типа параметр приводится к объекту, а не к строке. Таким образом, перед компилятором стоит задача присвоить Integer строке, что вызывает ClassCastException.

5. Заключение

В этой статье мы рассмотрели ряд распространенных сценариев неуместного приведения типов.

Неявное или явное приведение ссылок Java к другому типу может привести к ClassCastException, если только целевой тип не является тем же или потомком фактического типа.