Приведение и instanceof
Пульт «Животное» направлен на собаку, но кнопки «Лай» на нём нет, поэтому a.bark() не компилируется. Чтобы собака залаяла, нужен пульт «Собака». В Java такой пульт получают приведением: берут ту же ссылку и смотрят на неё как на ссылку типа Dog.
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
Dog d = (Dog) a;
d.bark(); // Woof
}
}
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {
void bark() { System.out.println("Woof"); }
}Давайте разберём программу по шагам:
Animal a = new Dog();кладёт ссылку на собаку в переменную типаAnimal. Это пульт «Животное».(Dog) a— приведение: тип в скобках перед ссылкой. Так мы говорим компилятору: «за этой ссылкой собака».Dog d = (Dog) a;кладёт ту же ссылку в переменную типаDog. Это пульт «Собака».d.bark()печатаетWoof: у типаDogметодbarkесть.
Запись со скобками знакома по числам: (int) 3.7 даёт 3. Но у чисел приведение меняет значение, а у ссылок — нет. Объект не меняется и не копируется: a и d ведут к одной и той же собаке, меняется только пульт.
Вверх и вниз по иерархии
Родителя принято рисовать сверху, а наследников — под ним. Отсюда и названия двух приведений.
Приведение вверх
Приведение вверх — от наследника к родителю: Animal a = new Dog();. Java делает его сама, без скобок. Собака всегда является животным, и ошибки здесь быть не может.
Приведение вниз
Приведение вниз — от родителя к наследнику: (Dog) a. Его пишут явно, потому что животное не всегда собака. Компилятор видит только тип Animal и верит вам на слово, а проверяет приведение JVM, когда программа доходит до этой строки.
Если за ссылкой окажется кошка, JVM бросит исключение ClassCastException и остановит программу на строке с приведением. А заведомо невозможное приведение отвергает уже компилятор: переменную типа Cat к Dog не привести, и он ответит incompatible types: Cat cannot be converted to Dog.
Проверка instanceof
Чтобы не гадать, тип объекта проверяют оператором instanceof. Он даёт true, если объект относится к этому классу или к его наследнику:
Animal b = new Cat();
System.out.println(a instanceof Dog); // true
System.out.println(b instanceof Dog); // false
System.out.println(b instanceof Animal); // trueРазберём по строкам:
a instanceof Dogдаётtrue: заaсобака.b instanceof Dogдаётfalse: заbкошка.b instanceof Animalдаётtrue: кошка тоже животное.
Для null ответ всегда false: за пустой ссылкой нет объекта.
Паттерн instanceof Dog dog
Раньше после проверки приходилось приводить отдельно: if (b instanceof Dog) { Dog dog = (Dog) b; … }. С Java 16 проверку и новую переменную пишут в одной записи:
if (b instanceof Dog dog) {
dog.bark();
} else {
System.out.println("not a dog"); // not a dog
}Запись b instanceof Dog dog называют паттерном. Она проверяет тип и, если проверка прошла, заводит переменную dog типа Dog с той же ссылкой. Переменная dog видна только там, где проверка точно прошла: в этом примере — в ветке if. Здесь за b кошка, поэтому выполняется else.
На собеседовании часто спрашивают, когда возникает ClassCastException. Ответ: при приведении вниз, если объект на самом деле другого класса. Защита от него — проверка instanceof перед приведением.
Здесь картинка с пультом неточна. Пульт «Собака» в жизни можно навести и на кошку: кнопка «Лай» ничего не сделает. Java так не делает: неверное приведение останавливает программу сразу, на строке с приведением.
Частая ошибка
Привели ссылку вниз, не проверив, что за ней:
Animal pet = new Animal();
Dog rex = (Dog) pet; // ✗Exception in thread "main" java.lang.ClassCastException: class Animal cannot be cast to class Dog (Animal and Dog are in unnamed module of loader 'app')Программа компилируется: pet объявлена как Animal, а животное может оказаться собакой. Но за pet стоит объект самого Animal, и JVM бросает ClassCastException.
Сообщение называет оба класса: class Animal cannot be cast to class Dog. В скобках JVM пишет, кто загрузил классы. Так сообщение выглядит после javac и java Main, а при запуске java Main.java в скобках будет другое имя.
Проверяйте тип перед приведением: if (pet instanceof Dog rex) { … }.
Главное
- Приведение вверх,
Animal a = new Dog(), Java делает сама, а приведение вниз,(Dog) a, пишут явно. - Приведение меняет только тип ссылки, а объект остаётся прежним.
- Если объект другого класса, приведение вниз бросает
ClassCastExceptionво время работы программы. x instanceof Dogпроверяет тип объекта, аif (x instanceof Dog d)вдобавок заводит переменнуюdтипаDog.