Привет! В одной из прошлых лекций мы обсуждали приведение примитивных типов. Давай вкратце вспомним, о чем шла речь. Мы представляли примитивные типы (в данном случае — числовые) в виде матрешек согласно объему памяти, которое они занимают. Как ты помнишь, поместить меньшую матрешку в большую будет просто как в реальной жизни, так и в программировании на 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. Такая операция называется явным преобразованием, или сужением.

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

Приведение типов (преобразование типов) — преобразование значения переменной одного типа в значение другого типа.

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

Сейчас мы поговорим о тех же операциях, но применимо не к примитивным типам, а к объектам и ссылочным переменным! Как же это работает в 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, которая идет по дереву наследования ниже. Логично, что без явного указания компилятор такую операцию не пропустит, но если в скобках указать тип, все заработает.

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

У нас есть некоторая иерархия классов. Тут видно все классы иерархии, кто кого наследует и методы каждого класса. Рисунок 1Есть расширяющее и сужающее приведение. Мы видим, что класс 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 два разных класса. Расширение и сужение ссылочных типов - 2 Рассмотрим другой пример, поинтереснее:

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();
Переменной-наследнику нельзя присвоить объект предка. Наоборот делать можно.

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

Рассмотрим код который я сделал по диаграмме выше "иерархия классов". Класс 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 + ": Meow!!! Meow!!!");
    }
}
Представьте себе ситуацию. Нам нужно собрать всех животных в один список, накормить их и потом уложить спать. Это легко сделать, если мы создадим 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 часто можно обойтись без явного приведения типов благодаря дженерикам и другим возможностям языка.