Интерфейсы, enum ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-29

Приведение ссылочных типов в Java

Строка HeavyBox1 heavy = (HeavyBox1) box; успешно компилируется, но если в box на самом деле лежит ColorBox, программа упадёт с ClassCastException во время выполнения. Компилятор проверяет только то, что приведение в принципе возможно, а реальный тип объекта известен лишь JVM - поэтому важно понимать, как устроено приведение ссылочных типов.

Приведение ссылочных типов в Java - это изменение типа ссылки на объект в пределах иерархии наследования: либо вверх, к суперклассу или интерфейсу (расширение, upcasting), либо вниз, к наследнику (сужение, downcasting). Сам объект при этом не меняется - меняется только то, через какой тип мы к нему обращаемся. Приведение примитивных типов мы рассматривали в предыдущем уроке; здесь речь пойдёт об объектах, операторе instanceof и массивах.

Во всех примерах используется следующая иерархия классов:

class Box6 {
    int width, height, depth;
}

class HeavyBox1 extends Box6 {
    int weight;
}

class ColorBox extends Box6 {
    String color;
}

1. Расширение типов в Java (upcasting)

Расширение, или восходящее преобразование (upcasting), - это переход от более конкретного типа к менее конкретному, то есть от наследника к родителю.

Как и в случае с примитивными типами, расширение выполняется автоматически, явный оператор приведения не нужен. Если класс Box - суперкласс, а HeavyBox - его наследник, то объект типа HeavyBox можно неявно присвоить переменной типа Box:

Box heavyBox = new HeavyBox(15, 10, 20, 5);

Расширение всегда безопасно: наследник гарантированно обладает всеми полями и методами родителя. Через переменную типа Box будут доступны только члены класса Box, но переопределённые методы всё равно вызовутся из HeavyBox - это и есть полиморфизм.

Также расширяющим является преобразование от null-типа к любому объектному типу:

Box box = null;

2. Сужение типов в Java (downcasting)

Обратный переход, то есть движение по дереву наследования вниз, к наследникам, называется сужением, или нисходящим преобразованием (downcasting). Оно требует явного оператора приведения (Тип). Переменную box типа Box можно привести к HeavyBox:

Box box = new HeavyBox();
HeavyBox heavyBox = (HeavyBox) box;

Следующий пример показывает, зачем нужны сужающие преобразования.

Допустим, переменная box1 типа Box6 указывает на объект типа HeavyBox1 (Box6 - суперкласс, HeavyBox1 - его наследник). Мы хотим вывести на консоль значение поля weight для объекта box1. Но поле weight объявлено в классе HeavyBox1, поэтому через ссылку типа Box6 доступа к нему нет. Чтобы обратиться к весу, нужно выполнить приведение к HeavyBox1: HeavyBox1 heavyBox1 = (HeavyBox1) box1.

При попытке привести к HeavyBox1 переменную box2, которая указывает на объект типа ColorBox, возникнет ошибка ClassCastException времени выполнения. Ошибки компиляции при этом не будет: ColorBox тоже наследник Box6, и компилятор не знает, какой объект окажется в переменной. То же самое произойдёт, если объект имеет тип самого суперкласса Box6.

public class CastingExample1 {
    public static void main(String[] args) {
        Box6 box1 = new HeavyBox1();
        // System.out.println(box1.weight); // ошибка компиляции

        HeavyBox1 heavyBox1 = (HeavyBox1) box1;
        System.out.println("Вес: " + heavyBox1.weight);

        Box6 box2 = new ColorBox();
        HeavyBox1 heavyBox2 = (HeavyBox1) box2; // ClassCastException

        Box6 box3 = new Box6();
        HeavyBox1 heavyBox3 = (HeavyBox1) box3; // ClassCastException
    }
}

Важно

Явное приведение — это обещание компилятору: «я знаю, что здесь лежит объект нужного типа». Компилятор ему верит, а проверку делает JVM во время выполнения. Если обещание не выполнено, вы получите ClassCastException, поэтому перед сужением тип обычно проверяют через instanceof.

3. Оператор instanceof в Java

Оператор instanceof проверяет, является ли объект экземпляром указанного класса, его наследника или класса, реализующего указанный интерфейс. Он возвращает true, если приведение к этому типу пройдёт без ClassCastException, и false в противном случае. Для null результат всегда false.

public class CastingExample2 {
    public static void main(String[] args) {
        Box6 box1 = new HeavyBox1();
        if (box1 instanceof HeavyBox1) {
            System.out.println("Приведение 1");
        }
        if (box1 instanceof Box6) {
            System.out.println("Приведение 2");
        }
        if (box1 instanceof Object) {
            System.out.println("Приведение 3");
        }

        Box6 box2 = new ColorBox();
        if (box2 instanceof HeavyBox1) {
            System.out.println("Приведение 4");
        }

        Box6 box3 = new Box6();
        if (box3 instanceof HeavyBox1) {
            System.out.println("Приведение 5");
        }
    }
}

Результат выполнения:

Приведение 1
Приведение 2
Приведение 3

Объект HeavyBox1 одновременно является HeavyBox1, Box6 и Object, поэтому первые три проверки дают true. Объекты ColorBox и Box6 не являются HeavyBox1 - строки 4 и 5 не выводятся. Именно в этих случаях приведение из CastingExample1 завершалось ClassCastException.

Типичный шаблон - сначала проверить тип, затем привести. В примере ниже Transport и Robot реализуют интерфейс Moveable:

public class CastingExample3 {
    public static void main(String[] args) {
        Moveable moveable1 = new Transport();
        if (moveable1 instanceof Transport) {
            Transport transport = (Transport) moveable1;
            transport.start();
        }

        Moveable moveable2 = new Robot();
        if (moveable2 instanceof Transport) {
            Transport transport = (Transport) moveable2;
            transport.stop();
        }
    }
}

Первая проверка проходит, и вызывается start(). Во второй ветке moveable2 ссылается на Robot, проверка возвращает false, и приведение не выполняется - исключения не будет.

3.1. Pattern matching для instanceof (Java 16+)

Начиная с Java 16, проверку и приведение можно объединить: после instanceof указывается имя переменной, которая уже имеет нужный тип (pattern matching for instanceof, JEP 394). Явный cast больше не нужен:

Box6 box = new HeavyBox1();
if (box instanceof HeavyBox1 heavy) {
    System.out.println("Вес: " + heavy.weight);
}

// можно использовать в условии сразу
if (box instanceof HeavyBox1 heavy && heavy.weight > 10) {
    System.out.println("Тяжёлый ящик");
}

Переменная heavy доступна только там, где проверка гарантированно прошла. В Java 21 тот же подход распространён на switch (pattern matching for switch), что позволяет разбирать несколько типов без цепочки if-else.

4. Несовместимые преобразования в Java

Приведение между классами возможно только внутри одной иерархии наследования. Если компилятор видит, что типы заведомо несовместимы, он выдаёт ошибку. Этот пример не скомпилируется, потому что Box6 и String никак не связаны:

Box6 box1 = new HeavyBox1();
String str = (String) box1; // ошибка компиляции: incompatible types

С интерфейсами правило мягче. Приведение к интерфейсу компилируется, даже если класс его не реализует, - при условии, что класс не final: у него может существовать наследник, реализующий интерфейс. Проверка тогда переносится на время выполнения:

Box6 box = new Box6();
Runnable r = (Runnable) box;      // компилируется, но ClassCastException при выполнении
// Runnable r2 = (Runnable) "abc"; // ошибка компиляции: String - final и не реализует Runnable

5. Преобразование типов для массивов

5.1. Массивы и примитивные типы

Переходы между массивами и примитивными типами запрещены:

public class ArrayCastingExample1 {
    public static void main(String[] args) {
        int[] array = new int[5];
        // int someNumber = array; // ошибка компиляции
        int someNumber = array[0];
    }
}

5.2. Примитивные и ссылочные массивы

Массив, основанный на примитивном типе, нельзя преобразовать к типу массива, основанному на ссылочном типе, и наоборот. Автоупаковка на массивы не распространяется:

public class ArrayCastingExample2 {
    public static void main(String[] args) {
        Integer[] array1 = new Integer[4];
        int[] array2 = new int[4];
        // array1 = array2; // ошибка компиляции
        // array2 = array1; // ошибка компиляции
    }
}

5.3. Массивы, основанные на разных примитивных типах

Преобразования между типами массивов, основанных на различных примитивных типах, невозможны, даже если сами элементы расширяются неявно (int в long):

public class ArrayCastingExample3 {
    public static void main(String[] args) {
        int[] array1 = new int[5];
        long[] array2 = new long[5];
        // array2 = array1; // ошибка компиляции
    }
}

5.4. Ссылочные массивы, основанные на типах из одной иерархии

Массив, основанный на типе HeavyBox, можно привести к массиву, основанному на типе Box, если сам тип HeavyBox приводится к типу Box. Обратное сужение компилируется, но проверяется во время выполнения: если реальный объект - массив Box6[], возникнет ClassCastException.

public class ArrayCastingExample4 {
    public static void main(String[] args) {
        rightConversion();
        wrongConversion();
    }

    private static void rightConversion() {
        Box6[] boxArray = new Box6[5];
        HeavyBox1[] heavyBoxArray = new HeavyBox1[6];
        boxArray = heavyBoxArray;
    }

    private static void wrongConversion() {
        Box6[] boxArray = new Box6[5];
        HeavyBox1[] heavyBoxArray = new HeavyBox1[6];
        heavyBoxArray = (HeavyBox1[]) boxArray; // ClassCastException
    }
}

5.5. Массивы и другие ссылочные типы

Массив можно привести к другому, не массивному, ссылочному типу только в трёх случаях: к классу Object и к интерфейсам Cloneable и Serializable, которые реализует любой массив.

import java.io.Serializable;

public class ArrayCastingExample5 {
    public static void main(String[] args) {
        Box6[] array = new Box6[5];
        Object object = array;
        Cloneable cloneable = array;
        Serializable serializable = array;
    }
}

5.6. Ошибка ArrayStoreException

Массивы в Java ковариантны: HeavyBox1[] можно присвоить переменной типа Box6[]. Но массив помнит свой реальный тип элементов, и при попытке записать в него объект неподходящего типа JVM выбрасывает ArrayStoreException:

public class ArrayCastingExample6 {
    public static void main(String[] args) {
        HeavyBox1[] heavyBox = new HeavyBox1[4];
        Box6[] box = heavyBox;
        box[0] = new Box6(); // ArrayStoreException
    }
}

Нюанс

Коллекции с дженериками устроены иначе: List<HeavyBox1> нельзя присвоить переменной List<Box6> — это ошибка компиляции. Так язык защищает от ситуации, которая у массивов проявляется только в виде ArrayStoreException во время выполнения.

6. Upcasting и downcasting: сравнение

Критерий Расширение (upcasting) Сужение (downcasting)
Направление От наследника к суперклассу или интерфейсу От суперкласса или интерфейса к наследнику
Синтаксис Неявно: Box6 b = new HeavyBox1(); Явно: (HeavyBox1) b
Безопасность Всегда безопасно Возможен ClassCastException
Доступные члены Только объявленные в суперклассе Все члены наследника
Проверка перед приведением Не нужна instanceof или pattern matching

Часто задаваемые вопросы

Меняется ли объект при приведении ссылочного типа?

Нет. Приведение меняет только тип ссылки, то есть набор полей и методов, доступных через неё. Объект в памяти остаётся тем же, и переопределённые методы всегда вызываются по его реальному классу, даже после upcasting.

Что вернёт instanceof для null и можно ли привести null?

Выражение null instanceof Любой_тип всегда возвращает false. При этом приведение (HeavyBox1) null допустимо и не выбрасывает исключения - результатом будет null.

Как избежать ClassCastException?

Перед сужением проверяйте тип через instanceof, а в Java 16+ используйте pattern matching: if (obj instanceof HeavyBox1 h). Ещё лучше по возможности обходиться без downcasting: выносить нужное поведение в методы суперкласса или интерфейса и использовать дженерики вместо Object.

Чем приведение ссылочных типов отличается от приведения примитивов?

При приведении примитивов меняется само значение: например, (int) 3.9 даёт 3, а сужение может привести к потере данных без всякой ошибки. При приведении ссылочных типов значение (объект) не меняется, а при несоответствии типов JVM выбрасывает ClassCastException. Подробнее о примитивах - в уроке Преобразование и приведение примитивных типов.

Почему приведение к интерфейсу компилируется, даже если класс его не реализует?

Если класс не final, у него может быть наследник, который реализует этот интерфейс, поэтому компилятор разрешает приведение и откладывает проверку до выполнения. Для final-класса, не реализующего интерфейс, приведение - ошибка компиляции.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.