Приведение ссылочных типов в 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-класса, не реализующего интерфейс, приведение - ошибка компиляции.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии