Перейти к содержимому
Теория · Наследование и полиморфизм · урок 9 из 10

Приведение и instanceof

Пульт «Животное» направлен на собаку, но кнопки «Лай» на нём нет, поэтому a.bark() не компилируется. Чтобы собака залаяла, нужен пульт «Собака». В Java такой пульт получают приведением: берут ту же ссылку и смотрят на неё как на ссылку типа Dog.

JAVA
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"); }
}

Давайте разберём программу по шагам:

  1. Animal a = new Dog(); кладёт ссылку на собаку в переменную типа Animal. Это пульт «Животное».
  2. (Dog) a — приведение: тип в скобках перед ссылкой. Так мы говорим компилятору: «за этой ссылкой собака».
  3. Dog d = (Dog) a; кладёт ту же ссылку в переменную типа Dog. Это пульт «Собака».
  4. 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, если объект относится к этому классу или к его наследнику:

JAVA
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

Разберём по строкам:

  1. a instanceof Dog даёт true: за a собака.
  2. b instanceof Dog даёт false: за b кошка.
  3. b instanceof Animal даёт true: кошка тоже животное.

Для null ответ всегда false: за пустой ссылкой нет объекта.

Паттерн instanceof Dog dog

Раньше после проверки приходилось приводить отдельно: if (b instanceof Dog) { Dog dog = (Dog) b; … }. С Java 16 проверку и новую переменную пишут в одной записи:

JAVA
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 так не делает: неверное приведение останавливает программу сразу, на строке с приведением.

Частая ошибка

Привели ссылку вниз, не проверив, что за ней:

JAVA
Animal pet = new Animal();
Dog rex = (Dog) pet; // ✗
TEXT
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.