User Sant9Iga
Sant9Iga
41 уровень

Преобразование ссылочных типов, или спящий волк на клавиатуре

Статья из группы Архив info.javarush.ru
Доброго времени суток, джаварашовец. Начали мне поступать вопросы о приведение ссылочных типов в Java. Что бы каждый раз не пересказывать одно и то же, решил написать маленькую статью. Преобразование ссылочных типов, или спящий волк на клавиатуре - 1

Сначала разберем, что же это такое

Приведение типов (преобразование типов) — преобразование значения переменной одного типа в значение другого типа. Давайте посмотрим на примере, что это такое и с чем его едят. У нас есть некоторая иерархия классов (см. Рисунок 1). Тут видно все классы иерархии, кто кого наследует и методы каждого класса. Рисунок 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 два разных класса. Что бы избежать ClassCastException при сужающем преобразовании, используйте instanceof.

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

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

Рассмотрим код который я сделал по диаграмме с Рис. 1. Класс 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. Это очень хорошо, потому что, если бы мы могли вызвать все методы, то зачем нам нужен волк который спит на клавиатуре, или йорик который ворует куриц? Спасибо за внимание. Надеюсь, эта статья для Вас будет полезна. Критика и комментарии приветствуются)
Комментарии (44)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
13 сентября 2021
Большое спасибо за статью!
Anonymous #2135740 Уровень 34
31 октября 2019
Йорик: "Meow!!! Meow!!" 😊
Кот коммунист Уровень 1
31 октября 2019
Великолепная статья
Nordis Уровень 28, Санкт-Петербург Expert
25 августа 2019
Пока читал кто от кого там наследуется , забыл кто там был первый и второй . Но потом посмотрел на рис 1 , и всё понял . Сделано по рисунку .
10 октября 2018
Как можно понять такое предложение? Переведите на человеческий русский пожалуйста. "При таком приведении мы не можем через ссылку animalCat/animalDog вызвать методы, которые есть в Cat/Dog, но которых нету в Animal."
RaccoonLady Уровень 13, Беларусь
30 июля 2018
List allAnimals = new ArrayList<>(); на эту строку компилятор ругается Не указан тип объектов, которые будут храниться в листе Должно быть List<Animal> allAnimals = new ArrayList<>();
Alex Fedoniuk Уровень 15
12 июня 2018
Код нечитабельный. Так не делается.
REDAS Уровень 21
17 августа 2017
О каком классе Dog идёт речь? Или ты считаешь Dog и YorkshireTerrier это одно и тоже? В программировании даже одна точка важна! Автор аутист, исправь или удали статью, с самого начала читать бессмысленно.
Neyron Уровень 40, Санкт-Петербург, Россия
16 сентября 2016
Доброго времени суток! Давайте отделим мух от котлет и разберём их по отдельности.
Мухи — переменные примитивного типа. У них нет ссылок, они просто занимают в памяти определённое количество байт. Тип byte занимает один байт ( 8 бит). Тип short требует два байта (16 бит). Теперь, если Вы переливаете из 8-и битного ведра в 16-и битное, то все биты перельются без потерь.
<code>
byte b = 127;
short s = b;
</code>
В данном случае приведение типов выполняется компилятором самостоятельно (неявно), т.е. нет необходимости писать short s = (short) b Мы преобразуем (расширяем) менее ёмкий по памяти тип до более ёмкого. Преобразования с расширением типов выполняются компилятором неявно.
В коде ниже происходит сужение типа соответственно. Часть битов из 16-и битного ведра даром пропадают при переливании в 8-и битное ведро.
<code>
short s = 2016;
byte b = (byte) s;
</code>
Происходит сужение типа, которое мы должны обозначить явно.
"-Пол литра в дребезги? Да?!
-Да!
-Да я тебя!!!..."
Теперь разберёмся с котлетами- переменными ссылочного типа.
Переменная указывает (хранит адрес в памяти) на экземпляр объекта (конкретные данные занимающие часть памяти в зависимости от архитектуры объекта). Ссылочная переменная должна иметь тип, т.к. Java — строго типизированный язык. В самом простом варианте тип ссылочной переменной при присвоении должен соответствовать классу на основе которого создаётся экземпляр объекта:
<code>
A instance = new A();
</code>
В свете наследования классов друг от друга и полиморфизма всё усложняется. При приведении переменной типа потомок к переменной типа родитель мы теряем те поля, которые были определены только в потомке, т.е. обращаясь к объекту потомок через ссылочную переменную типа родитель, мы увидим только те переменные и методы, которые были унаследованы
toMy Уровень 26, Россия
12 апреля 2016
Огромное спасибо за статью, все доступно и приятно читать. В лекции JR все наоборот написано, что меня и привело к вам)