Привет! В одной из прошлых лекций мы обсуждали приведение примитивных типов. Давай вкратце вспомним, о чем шла речь. Мы представляли примитивные типы (в данном случае числовые) в виде матрешек согласно объему памяти, которое они занимают. Как ты помнишь, поместить меньшую матрешку в большую будет просто как в реальной жизни, так и в программировании на Java.

public class Main {
   public static void main(String[] args) {
        short smallNumber = 100;
        int bigNumber =  smallNumber;
        System.out.println(bigNumber);
   }
}
Это пример автоматического преобразования, или расширения. Оно происходит само по себе, поэтому дополнительный код писать не нужно. В конце концов, мы не делаем ничего необычного: просто кладем матрешку поменьше в матрешку побольше. Другое дело, если мы попытаемся сделать наоборот и положить большую матрешку в меньшую. В жизни такое сделать нельзя, а в программировании можно. Но есть один нюанс. Если мы попытаемся положить значение int в переменную short, у нас это так просто не выйдет. Ведь в переменную short поместится всего 16 бит информации, а значение int занимает 32 бита! В результате передаваемое значение исказится. Компилятор выдаст нам ошибку («чувак, ты делаешь что-то подозрительное!»), но если мы явно укажем, к какому типу приводим наше значение, он все-таки выполнит такую операцию.

public class Main {

   public static void main(String[] args) {

       int bigNumber = 10000000;

       bigNumber = (short) bigNumber;

       System.out.println(bigNumber);

   }

}
В примере выше мы так и поступили. Операция выполнена, но поскольку в переменную short поместилось только 16 бит из 32, итоговое значение было искажено, и в результате мы получили число -27008. Такая операция называется явным преобразованием, или сужением.

Что такое приведение типов

Приведение типов (преобразование типов) это преобразование значения переменной одного типа в значение другого типа. Если коротко: расширение (upcasting) это присваивание объекта-потомка ссылке родительского типа, например Animal animal = new Pet(). Оно происходит автоматически. Сужение (downcasting) это обратное движение вниз по дереву наследования, например Pet pet = (Pet) animal: оно требует явного приведения в скобках и может закончиться ошибкой ClassCastException, если реальный тип объекта не подходит. Чтобы этого избежать, перед сужением проверяют тип оператором instanceof.

Кратко

  • Приведение ссылочных типов работает только между классами, связанными наследованием.
  • Расширение (upcasting): объект-потомок присваивается ссылке родителя, происходит автоматически.
  • Сужение (downcasting): обратное приведение вниз по иерархии, требует явного (Тип) и может выбросить ClassCastException.
  • ClassCastException возникает, если реальный тип объекта не является тем, к которому его приводят.
  • Перед сужением тип безопасно проверять оператором instanceof.
  • С Java 16 есть pattern matching: if (obj instanceof Cat cat) сразу дает переменную нужного типа без явного приведения.

Примеры расширения и сужения ссылочных типов

Сейчас мы поговорим о тех же операциях, но применимо не к примитивным типам, а к объектам и ссылочным переменным! Как же это работает в Java? На самом деле, довольно просто. Есть объекты, которые не связаны между собой. Было бы логично предположить, что их нельзя преобразовать друг в друга ни явно, ни автоматически:

public class Cat {
}

public class Dog {
}

public class Main {

   public static void main(String[] args) {

       Cat cat = new Dog();//ошибка!

   }

}
Здесь мы, конечно, получим ошибку. Классы Cat и Dog между собой не связаны, и мы не написали «преобразователя» одних в других. Логично, что сделать это у нас не получится: компилятор понятия не имеет, как конвертировать эти объекты между собой. Другое дело, если объекты будут между собой связаны! Как? Прежде всего, с помощью наследования. Давай попробуем создать небольшую систему классов с наследованием. У нас будет общий класс, обозначающий животных:

public class Animal {
  
   public void introduce() {

       System.out.println("i'm Animal");
   }
}
Животные, как известно, бывают домашними и дикими:

public class WildAnimal extends Animal {

   public void introduce() {

       System.out.println("i'm WildAnimal");
   }
}

public class Pet extends Animal {

   public void introduce() {

       System.out.println("i'm Pet");
   }
}
Для примера возьмем собачек: домашнего пса и койота:

public class Dog extends Pet {

   public void introduce() {

       System.out.println("i'm Dog");
   }
}





public class Coyote extends WildAnimal {

   public void introduce() {

       System.out.println("i'm Coyote");
   }
}
Классы у нас специально самые примитивные, чтобы легче было воспринимать их. Поля нам тут особо не нужны, а метода хватит и одного. Попробуем выполнить вот такой код:

public class Main {

   public static void main(String[] args) {

       Animal animal = new Pet();
       animal.introduce();
   }
}
Как ты думаешь, что будет выведено на консоль? Сработает метод introduce класса Pet или класса Animal? Попробуй обосновать свой ответ, прежде чем продолжать чтение. А вот и результат! i'm Pet Почему ответ получился таким? Все просто. У нас есть переменная-родитель и объект-потомок. Написав:

Animal animal = new Pet();
мы произвели расширение ссылочного типа Pet и записали его объект в переменную Animal. Как и в случае с примитивными, расширение ссылочных типов в Java производится автоматически. Дополнительный код для этого писать не нужно. Теперь у нас к ссылке-родителю привязан объект-потомок, и в итоге мы видим, что вызов метода производится именно у класса-потомка. Если ты все еще не до конца понимаешь, почему такой код работает, перепиши его простым языком:

Животное животное = new ДомашнееЖивотное();
В этом же нет никаких проблем, правильно? Представь, что это реальная жизнь, а ссылка в данном случае это простая бумажная бирка с надписью «Животное». Если ты возьмешь такую бумажку и прицепишь на ошейник любому домашнему животному, все будет в порядке. Любое домашнее животное все равно животное! Обратный процесс, то есть движение по дереву наследования вниз, к наследникам, это сужение:

public class Main {

   public static void main(String[] args) {

       WildAnimal wildAnimal = new Coyote();

       Coyote coyote = (Coyote) wildAnimal;

       coyote.introduce();
   }
}
Как видишь, здесь мы явно указываем к какому классу хотим привести наш объект. Ранее у нас была переменная WildAnimal, а теперь Coyote, которая идет по дереву наследования ниже. Логично, что без явного указания компилятор такую операцию не пропустит, но если в скобках указать тип, все заработает.

Давайте посмотрим на примере, что такое приведение типов и с чем его едят.

У нас есть некоторая иерархия классов. Тут видно все классы иерархии, кто кого наследует и методы каждого класса. Диаграмма иерархии классов: Animal, WildAnimal, Pet и их наследникиЕсть расширяющее и сужающее приведение. Мы видим, что класс Cat является наследником класса Pet. Pet, в свою очередь, наследник класса Animal. Когда мы напишем:

Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Это расширяющее приведение (или неявное). Мы расширили ссылки animalCat и animalDog. Они ссылаются на объекты Cat и Dog. При таком приведении мы не можем через ссылку animalCat/animalDog вызвать методы, которые есть в Cat/Dog, но которых нету в Animal. Сужающее приведение(или явное) происходит в обратную сторону:

Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Cat cat =(Cat)animalCat;
YorkshireTerrier dog = (YorkshireTerrier) animalDog;
Мы явно указали к какому типу хотим привести данный объект. BUT, BE CAREFUL!!! Если Вы сделаете вот так:

Animal animalCat = new Cat();
YorkshireTerrier dog = (YorkshireTerrier) animalCat;
компилятор пропустит этот код. А вот RunTime выкинет вам:

Exception in thread "main" java.lang.ClassCastException: Animals.Cat cannot be cast to Animals.YorkshireTerrier
RunTime видит, что Cat и YorkshireTerrier два разных класса. Ошибка ClassCastException при неверном сужающем приведении Рассмотрим другой пример, поинтереснее:

public class Main {

   public static void main(String[] args) {

       Pet pet = new Animal();//ошибка!
   }
}
Компилятор выдает ошибку! В чем же причина? В том, что ты пытаешься присвоить переменной-потомку объект-родителя. Иными словами, ты хочешь сделать вот так:

ДомашнееЖивотное домашнееЖивотное = new Животное();
Но, может быть, если мы явно укажем тип, к которому пытаемся сделать приведение, у нас все получится? С числами вроде получилось, давай попробуем! :)

public class Main {

   public static void main(String[] args) {
      
       Pet pet = (Pet) new Animal();
   }
}
Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Pet Ошибка! Компилятор в этот раз ругаться не стал, однако в результате мы получили исключение. Причина нам уже известна: мы пытаемся присвоить переменной-потомку объект-родителя. А почему, собственно, нельзя этого делать? Потому что не все Животные являются ДомашнимиЖивотными. Ты создал объект Animal и пытаешься присвоить его переменной Pet. Но, к примеру, койот тоже является Animal, но он не является Pet, домашним животным. Иными словами, когда ты пишешь:

Pet pet = (Pet) new Animal();
На месте new Animal() может быть какое угодно животное, и совсем не обязательно домашнее! Естественно, твоя переменная Pet pet подходит только для хранения домашних животных (и их потомков), и не для всех подряд. Поэтому для таких случаев в Java было создано специальное исключение ClassCastException, ошибка при приведении классов.

Безопасное приведение типов с instanceof

Чтобы избежать ClassCastException, в Java есть специальный оператор instanceof, который проверяет, является ли объект экземпляром определенного класса. Что бы избежать ClassCastException при сужающем преобразовании, используйте instanceof.

Animal animalCat = new Cat();
if (animalCat instanceof YorkshireTerrier)
{
    YorkshireTerrier dog = (YorkshireTerrier) animalCat;
}
Если animalCat является YorkshireTerrier, тогда произойдет присвоение, если нет, ничего не произойдет.

Pattern Matching для instanceof

Начиная с Java 16, появилась более удобная возможность работы с instanceof, Pattern Matching. Это современный подход, который упрощает код:
// Старый подход (до Java 16)
if (animal instanceof Cat) {
    Cat cat = (Cat) animal;  // явное приведение
    cat.sleepOnKeyboard();
}

// Современный подход (Java 16+)
if (animal instanceof Cat cat) {  // сразу создается переменная cat
    cat.sleepOnKeyboard();
}
Как видишь, в новом синтаксисе мы сразу получаем переменную нужного типа без явного приведения. Это делает код короче и безопаснее. Давай проговорим еще раз, чтобы было понятнее. Переменная(ссылка)-родитель может указывать на объект класса-потомка:

public class Main {

   public static void main(String[] args) {

       Pet pet =  new Pet();
       Animal animal = pet;

       Pet pet2 = (Pet) animal;
       pet2.introduce();
   }
}
Например, здесь у нас проблем не возникнет. У нас есть объект Pet, на который указывает ссылка Pet. Потом на этот же объект стала указывать новая ссылка Animal. После чего мы делаем преобразование animal в Pet. Почему у нас это получилось, кстати? В прошлый раз мы получили исключение! Потому что в этот раз наш изначальный объект это Pet pet!

Pet pet =  new Pet();
А в прошлом примере это был объект Animal:

Pet pet = (Pet) new Animal();
Переменной-наследнику нельзя присвоить объект предка. Наоборот делать можно.

Расширение и сужение: сравнение

Соберем обе операции в одну таблицу.
СвойствоРасширение (upcasting)Сужение (downcasting)
Направление по иерархиивверх, к предкувниз, к потомку
Синтаксисавтоматически, без приведенияявное приведение (Тип) в скобках
Безопасностьвсегда безопасноможет выбросить ClassCastException
Проверка типане нужнажелательно instanceof перед приведением
ПримерAnimal a = new Cat();Cat c = (Cat) a;

А теперь зачем же это нужно, если мы теряем методы и можем получить такие ошибки

Рассмотрим код который я сделал по диаграмме выше "иерархия классов". Класс Animal

public abstract class Animal
{
    String name;
    int age;
    String nameOfClass = getClass().getSimpleName();
    public void eat(){
        System.out.println(nameOfClass + ": Omnomnom");
    }
    public void sleep(){
        System.out.println(nameOfClass + ": Z-z-z-z");
    }
}
Класс WildAnimal который наследуется от Animal

public abstract class WildAnimal extends Animal
{
    public void steelChicken()
    {
        System.out.println(nameOfClass+": Muhaha,I stole a chicken!");
    }
}
Класс Pet который наследуется от Animal

public abstract class Pet extends Animal
{
    public void peeInTray(){
        System.out.println(nameOfClass + ": Master, I peed");
    }
}
Класс Fox который наследуется от WildAnimal

public class Fox extends WildAnimal
{
    public void eatColobok(){
        System.out.println(nameOfClass + ": I will eat you, Colobok");
    }
}
Класс Wolf который наследуется от WildAnimal

public class Wolf extends WildAnimal
{
    public void hawlAtTheMoon(){
        System.out.println(nameOfClass + ": Ouuuuu!!!Ouuuu!!!");
    }
}
Класс Cat который наследуется от Pet

public class Cat extends Pet
{
    public void sleepOnKeyboard(){
        System.out.println(nameOfClass + ": Master, stop working!!I wanna sleep on your keyboard");
    }
}
Класс YorkshireTerrier который наследуется от Pet

public class YorkshireTerrier extends Pet
{
    public void bark(){
        System.out.println(nameOfClass + ": Woof!!! Woof!!!");
    }
}
Представьте себе ситуацию. Нам нужно собрать всех животных в один список, накормить их и потом уложить спать. Это легко сделать, если мы создадим ArrayList животных (Animal). И потом у каждого животного вызовем соответствующие методы:

public class ZOO
{
    public static void main(String[] args)
    {
        List<Animal> allAnimals = new ArrayList<>();
        allAnimals.add(new Cat());
        allAnimals.add(new Wolf());
        allAnimals.add(new Fox());
        allAnimals.add(new YorkshireTerrier());
        for (Animal animal : allAnimals)
        {
            animal.eat();
            animal.sleep();
        }
    }
}
Я не могу у animal вызвать метод bark() или sleepOnKeyboard(). Потому что в листе allAnimals хранятся кот, волк, йорик и лиса, но они приведены к Animal. И у них есть только те методы, которые есть в Animal. Это очень хорошо, потому что, если бы мы могли вызвать все методы, то зачем нам нужен волк который спит на клавиатуре, или йорик который ворует куриц?

Когда приведение типов не нужно

В современной Java часто можно обойтись без явного приведения типов благодаря дженерикам и другим возможностям языка.

Вопросы и ответы

Что такое расширение (upcasting) ссылочного типа?

Это присваивание объекта-потомка ссылке родительского типа, например Animal animal = new Cat(). Расширение происходит автоматически, без явного приведения.

Что такое сужение (downcasting)?

Это обратное приведение вниз по дереву наследования, например Cat cat = (Cat) animal. Оно требует явного указания типа в скобках и может выбросить ClassCastException.

Почему возникает ClassCastException?

Когда реальный тип объекта не является тем, к которому его приводят. Например, (Pet) new Animal() упадет, потому что не всякое Animal это Pet.

Как безопасно выполнить сужение типа?

Проверить тип оператором instanceof перед приведением. Начиная с Java 16 удобнее pattern matching: if (obj instanceof Cat cat) сразу дает переменную нужного типа.

Можно ли привести друг к другу несвязанные классы, например Cat и Dog?

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

Зачем вообще нужно приведение типов?

Чтобы хранить разные объекты в одной коллекции родительского типа, например List, и работать с ними единообразно, а при необходимости приводить к конкретному типу для вызова его методов.

Читайте также