Приведение типов. Расширение и сужение

Открыта

— Привет, Амиго! Тема сегодняшней лекции – расширение и сужение типов. С расширением и сужением примитивных типов ты познакомился уже давно. На 10 уровне. Сегодня мы расскажем, как это работает для ссылочных типов, т.е. для объектов классов.

Тут все довольно просто на самом деле. Представь себе цепочку наследования класса: класс, его родитель, родитель родителя и т.д. до самого класса Object. Т.к. класс содержит все методы класса, от которого он был унаследован, то объект этого класса можно сохранить в переменную любого из его типов родителей.

Пример:

Код Описание
class Pet
{
public void doPetActions();
}class Cat extends Pet
{
public void doCatActions();
}class Tiger extends Cat
{
public void doTigerActions();
}
Тут мы видим три объявленных класса: животное, кот и тигр. Кот наследуется от Животного. А Тигр от Кота.
public static void main(String[] args)
{
Tiger tiger = new Tiger();
Cat cat = new Tiger();
Pet pet = new Tiger();
Object obj = new Tiger();
}
Объект класса Tiger всегда можно спокойно присвоить переменной с типом класса-родителя. Для класса Tiger – это Cat, Pet и Object.

Теперь рассмотрим, что же такое расширение и сужение типов.

Если в результате присваивания мы двигаемся по цепочке наследования вверх (к типу Object), то это — расширение типа (оно же — восходящее преобразование или upcasting), а если вниз, к типу объекта, то это — сужение типа (оно же — нисходящее преобразование или downcasting).

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

Код Описание
public static void main(String[] args)
{
Object obj = new Tiger();
Pet pet = (Pet) obj;
Cat cat = (Cat) obj;
Tiger tiger = (Tiger) pet;
Tiger tiger2 = (Tiger) cat;
}
При сужении типа, нужно использовать оператор преобразования типа, то есть мы выполняем явное преобразование.

При этом Java-машина выполняет проверку, а действительно ли данный объект унаследован от Типа, к которому мы хотим его преобразовать.

Такое небольшое нововведение уменьшило количество ошибок в преобразовании типов в разы, и существенно повысило стабильность работы Java-программ.

16
Задача
Java Core,  4 уровень,  3 лекция
Недоступна
Набираем код
Внимание! Объявляется набор кода на JavaRush. Чтобы поучаствовать, включите режим повышенной внимательности, немного разомните пальцы, затем — расслабьте их и… набирайте код в соответствующем окошке. На самом деле это довольно полезное занятие, а не пустая трата времени, как может показаться.
Код Описание
public static void main(String[] args)
{
Object obj = new Tiger();
if (obj instanceof Cat)
{
Cat cat = (Cat) obj;
cat.doCatActions();
}}
Еще лучше – использовать проверку instanceof
public static void main(String[] args)
{
Pet pet = new Tiger();
doAllAction(pet);

Pet pet2 = new Cat();
doAllAction(pet2);

Pet pet3 = new Pet();
doAllAction(pet3);
}

public static void doAllAction(Pet pet)
{
if (pet instanceof Tiger)
{
Tiger tiger = (Tiger) pet;
tiger.doTigerActions();
}

if (pet instanceof Cat)
{
Cat cat = (Cat) pet;
cat.doCatActions();
}

pet.doPetActions();
}
И вот почему. Смотрим на пример слева.

Мы (наш код) не всегда знаем, с объектом какого типа мы работаем. Это может быть как объект того же типа, что и переменная (Pet), так и любой тип-наследник (Cat, Tiger).

Рассмотрим метод doAllAction. Он корректно работает в независимости от того, объект какого типа в него передали.

Т.е. он корректно работает для всех трех типов Pet, Cat, Tiger.

public static void main(String[] args)
{
Cat cat = new Tiger();
Pet pet = cat;
Object obj = cat;
}
Тут мы видим три присваивания. Все они являются примерами расширения типа.

Оператор преобразования типа тут не нужен, так как не нужна проверка. Ссылку на объект всегда можно сохранить в переменную любого его базового типа.

— О, на предпоследнем примере все стало понятно. И для чего нужна проверка, и для чего нужно преобразование типов.

— Надеюсь, что так. Хочу обратить твое внимание на следующую вещь:

С объектом при таком присваивании ничего не происходит! Меняется только количество методов, которое можно вызвать с помощью конкретной переменной-ссылки.

Например, переменная класса Cat позволяет вызывать методы doPetActions & doCatActions, и ничего не знает о методе doTigerActions, даже если ссылается на объект класса Tiger.

— Ну, это-то уже ясно. Оказалось легче, чем я думал.

Комментарии (109)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий вы должны авторизоваться
Алексей Клоков16 уровень, Москва
11 мая, 06:09
/* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Расширение */ Cat cat = new Tiger(); Любой Tiger есть Cat (можно писать new) Cat cat - создаем переменную cat класса Cat которая будет хранить ссылку на объект. new Tiger - создаем экземпляр класса Tiger и возвращаем ссылку на него. Java-машина выделяет память под созданный объект класса Tiger и присваивает ссылку на него переменной cat класса Cat. Но объект new Tiger ничего не знает о себе, потому что Cat ничего не знает о Tiger. Получается, что new Tiger может пользоваться всеми "приблудами" класса Cat и классов от которых он унаследован в плоть до Object, но ничего не знает о себе. /* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Cужение */ Tiger tiger = (Tiger) Pet(); Не всякий Pet есть Tiger (писать new нельзя) Tiger tiger - создаем переменную tiger класса Tiger которая будет хранить ссылку на объект. new Pet - создаем экземпляр класса Pet и ссылку на него сохраняем в Tiger. Т.е. объект new Pet() знает о себе только то, что описано в рамках класса Tiger() (если в классе Pet() есть метод который не переопределен в Tiger, то созданный объект не может его использовать). Поправьте меня если не прав
АртемGeek18 уровень, Москва
26 апреля, 06:03
Правильно ли я понял, у нас идет наследование кошка > питомец > животное. Если мы сделаем расширение кошки до животного:
Animal animal = new Cat();
то мы не сможем вызывать метода кошки? только методы животного? именно поэтому мы деалем проверку перед запуском метода и производим даункастинг:
if (obj instanceof Cat)
{
Cat cat = (Cat) obj;
cat.doCatActions();
}}
Павел Рожкин16 уровень
2 мая, 20:48
Да, правильно.
Dmytro Tretiakov14 уровень
10 мая, 09:50
Я так понимаю, что в примере выше Animal animal = new Cat(); у нас наоборот происходит сужение Animal до Cat, а не расширение кошки до животного. Было бы Cat cat = new Animal(); то тогда было бы расширение. Точно так же, как и с List<> list = new ArrayList<>();
АртемGeek18 уровень, Москва
10 мая, 10:21
В вопросе я разобрался, чуть позже будет подробное объяснение по лекциям. Вообще я понял, надо проходить пару лекций вперед и если не понятно уже спрашивать) тк все здесь наперед дается порой без начального подробного объяснения.
Animal animal = new Cat();
Здесь все правильно , идет расширение. Объект класса Cat присваивается переменной с типом класса-родителя (Animal ) Если бы это было сужение, то мы могли бы использовать методы Cat, в данном случае и всегда, мы теряем такую возможность потому что идет расширение по цепочке вверх.
Dmytro Tretiakov14 уровень
10 мая, 12:34
Ага, вот как... Понял, буду тогда ждать объяснения в следующих лекциях :) Очень жаль, что у них некорректно структурирован материал. Обрывками даётся информация то там, то тут... То, что должно было объясняться в самом начале, объясняется где-то в конце.
Nordis19 уровень, Санкт-Петербург
4 апреля, 04:38
Короче , я так понял что объект унаследованного класса можно создать как объект класса родителя. И объект созданный таким образом не будет иметь доступ к методам своего же класса. Пока что не понимаю зачем это нужно.
Игорь22 уровень, Минск
7 апреля, 21:25
Грубо говоря, есть набор музыкальных инструментов, унаследованных от базового абстрактного класса с абстрактным методом tune(). В классах конкретных инструментов у метода будет своя реализация, т.к. на гитаре одним способом играют, на трубе другим, на скрипке третьим и т.п. Смысл в том, что ими можно напичкать, скажем, ArrayList<общий предок>, а потом вызывать этот метод поочерёдно у каждого элемента коллекции. При этом мы вообще можем не знать, какой именно инструмент в какой момент нам попадётся, но будет вызвана реализация каждого конкретного инструмента. И это только один из многих вариантов использования полиморфизма как такового. Другой пример (из жизни отчасти, кстати) - это когда вы завязали логику на ArrayList, а потом профилирование показало, что там огромное количество вставок в середины списка, и производительность в итоге сильно проседает под нагрузкой. LinkedList здесь был бы куда лучше, где не нужно было бы делать тучу сдвигов, а просто поменять ссылки. Что в итоге? Если ссылка на объект была изначально как ArrayList, то придётся потом шариться везде в коде и менять все ссылки на LinkedList, а потом молиться, чтобы нигде ничего не грохнулось (а если Вы не один?). А можно было бы объявить ссылку на объект как List, а уже присвоить ему new ArrayList(), и тогда в этой же ситуации достаточно будет поменять new ArrayList() на new LinkedList() в том участке кода, где создаётся этот объект, и вообще не париться, что кто-то там где-то его использует. Реализация разная, но обе структуры являются списками (List), а значит и имеют одинаковые способы взаимодействия, пусть и с разными конкретными реализациями для тех или иных случаев.
Dmitry Sokol14 уровень, Минск
29 марта, 14:15
ничего не ясно амиго врал
Vladislav Polekha27 уровень, Penza
7 марта, 15:20
ищите внизу коментарий левомеколя.
Dmitry18 уровень, Екатеринбург
24 марта, 20:34
Или просто отсортируйте посты по популярности.
NewBuy22 уровень, Санкт-Петербург
25 февраля, 18:26
И вот почему. Смотрим на пример слева. Этот пример как-то связан с ООП?
Максим25 уровень, Минск
13 февраля, 20:15
В первом примере ошибка: вместо объявления тела методов классов Pet, Cat, Tiger в виде фигурных скобок стоит точка с запятой. Если это так, то метод, также как и его класс, должен быть объявлен абстрактным, соответственно от такого класса нельзя создать его экземпляр.
Кирилл Манжос16 уровень
26 февраля, 07:30
Я думаю что они хотели чтоб мы суть уловили, хотя это ошибка грубая и поставить вместо ';' '{}' труда не составило бы)
VIKTOR NEZHELSKIY27 уровень
10 февраля, 16:04
Итак: Тигр = это и кошка, и животное, и вообще какой-то объект (Object). Назвав его так мы употребляем всё более широкие понятия. По-этому Object obj = new Tiger(); - расширение типа всегда сработает для более широких понятий тигра (родителей его класса). Но применяя расширенное понятие тигра, например употребив к объекту понятие "животное", истина будет только если этот объект был по рождению как минимум животным, хоть кошкой, хоть тигром. Но и дерево тоже объект и к нему понятие "животное" не работает (его класс не относится к животным). По-этому необходимо сужение типа уточнением: Pet pet = (Pet) obj; Или для начала проверить, действительно-ли объект животное: if (obj instanceof Pet). - правда ли, что объект можно назвать животным. if (obj instanceof Cat). - правда ли, что объект можно назвать кошкой. if (obj instanceof Tiger). - правда ли, что объект можно назвать тигром. Для объекта, который тигр по происхождению это будет истиной во всех трёх проверках, а для объекта дерева по происхождению - ни разу истиной не будет.
Dmitry Golyshkin22 уровень, Москва
14 января, 08:23
Главное не начать читать комменты, если понял лекцию.
Sergo17 уровень, Санкт-Петербург
14 января, 19:36
и правда не стоит
Виталий Злобин18 уровень, Новосибирск
11 января, 10:46
В практике такое применяется или эта лекция для понимания: что может произойти и откуда ноги растут если...?
Sergo17 уровень, Санкт-Петербург
14 января, 19:33
Ну я например подошел к пониманию лямбд. Расширения класса до объекта типа интерфейса и следовательно мы все реализации методов берем из интерфейса, а так как у него нет реализации, кроме дефолтных, мы получаем необходимость прописать ее самому