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

Открыта

— Привет, Амиго! Тема сегодняшней лекции – расширение и сужение типов. С расширением и сужением примитивных типов ты познакомился уже давно. На 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.

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

Комментарии (119)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий вы должны авторизоваться
Artur15 уровень, Tallinn
суббота, 15:51
Эта фраза вводит в заблужение: Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании. Может лучше было написать так: Но при этом теряется возможность вызвать методы, которые не были унаследованы от класса-родителя (этого самого "общего типа"). Ведь при наследовании как раз и добавляются методы родителя (автоматически). Возможно я что-то путаю, поправьте плз.
Ваня Петило15 уровень, Львов
11 июля, 21:29
Мне одному немного режет глаза то, как написан первый пример? Я о стиле написания
Vgoose16 уровень, Москва
28 июня, 20:46
Понравилось пояснение про наследование, доступа к методам и размещение класса в памяти. Наследование Дочерний класс в памяти
Павел17 уровень, Москва
18 июня, 19:38
Ничего не понял.. Например, переменная класса Cat позволяет вызывать методы doPetActions & doCatActions, и ничего не знает о методе doTigerActions, даже если ссылается на объект класса Tiger. То есть, авторы имеют ввиду, что если написать что-то из серии
Cat cat = new Tiger(); // Тигр является наследником Cat
То у объекта cat можно вызывать только методы класса Cat? методы класса Тигр работать не будут? Нет, объекту cat доступны все методы класса Tiger. Это можно проверить в Intel JS и убедиться, что методы доступны. Да даже на предыдущем уровне это обсуждали или тут что-то другое имели ввиду?
y-grek14 уровень, Киев
19 июня, 19:52
твой пример (скрин) о переопределении методов.. метод гетНейм(); мы можем вызвать потому, как он есть и у родителя.. а работает он по своему ("я - кит") потому что переопределен в классе дитяти ))) (потомка)
Иван Ващенко14 уровень, Москва
8 июля, 14:38
мне кажется, они имели ввиду уже после неявного расширения типа: Pet pet = cat; в этом случае не будет у него доступа к методам объекта Tiger. Поправьте если не так.
Ваня Петило15 уровень, Львов
11 июля, 21:46
...видимо да, ведь: Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании. Если я правильно понял, то у класса Кот, при создании которого мы ссылались на Тигр, не будет доступа к новым методам, которые есть только у класса Тигр. То есть, будет доступ к своим методам и тем, что были переопределены в классе Тигр. Хотелось бы услышать мнение знающих людей, мы с автором коммента правильно поняли эту часть статьи?
Anonymous #102274624 уровень, Москва
14 июня, 10:41
Там после названия метода, наверно, все же лучше скобочки поставить и что-нибудь в них написать. А А так - не очень корректный код получается. Ошибка компиляции. Метод без реализации, значит класс тогда должен абстрактным быть.
Алексей Клоков20 уровень, Москва
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, то созданный объект не может его использовать). Поправьте меня если не прав
Veronika Nikitina17 уровень, Санкт-Петербург
20 мая, 04:31
Про не переопределенный что-то не то. Наследники могут пользоваться всеми методами родителя, но могут переопределять их и использовать уже новые. Т.е. методы описанные в pet и непереопределенные в tiger этому самому tigerу то доступны
АртемGeek25 уровень, Москва
26 апреля, 06:03
Правильно ли я понял, у нас идет наследование кошка > питомец > животное. Если мы сделаем расширение кошки до животного:
Animal animal = new Cat();
то мы не сможем вызывать метода кошки? только методы животного? именно поэтому мы деалем проверку перед запуском метода и производим даункастинг:
if (obj instanceof Cat)
{
Cat cat = (Cat) obj;
cat.doCatActions();
}}
Павел Рожкин18 уровень
2 мая, 20:48
Да, правильно.
Dmytro Tretiakov20 уровень
10 мая, 09:50
Я так понимаю, что в примере выше Animal animal = new Cat(); у нас наоборот происходит сужение Animal до Cat, а не расширение кошки до животного. Было бы Cat cat = new Animal(); то тогда было бы расширение. Точно так же, как и с List<> list = new ArrayList<>();
АртемGeek25 уровень, Москва
10 мая, 10:21
В вопросе я разобрался, чуть позже будет подробное объяснение по лекциям. Вообще я понял, надо проходить пару лекций вперед и если не понятно уже спрашивать) тк все здесь наперед дается порой без начального подробного объяснения.
Animal animal = new Cat();
Здесь все правильно , идет расширение. Объект класса Cat присваивается переменной с типом класса-родителя (Animal ) Если бы это было сужение, то мы могли бы использовать методы Cat, в данном случае и всегда, мы теряем такую возможность потому что идет расширение по цепочке вверх.
Dmytro Tretiakov20 уровень
10 мая, 12:34
Ага, вот как... Понял, буду тогда ждать объяснения в следующих лекциях :) Очень жаль, что у них некорректно структурирован материал. Обрывками даётся информация то там, то тут... То, что должно было объясняться в самом начале, объясняется где-то в конце.
Nordis23 уровень, Санкт-Петербург
4 апреля, 04:38
Короче , я так понял что объект унаследованного класса можно создать как объект класса родителя. И объект созданный таким образом не будет иметь доступ к методам своего же класса. Пока что не понимаю зачем это нужно.
Игорь22 уровень, Минск
7 апреля, 21:25
Грубо говоря, есть набор музыкальных инструментов, унаследованных от базового абстрактного класса с абстрактным методом tune(). В классах конкретных инструментов у метода будет своя реализация, т.к. на гитаре одним способом играют, на трубе другим, на скрипке третьим и т.п. Смысл в том, что ими можно напичкать, скажем, ArrayList<общий предок>, а потом вызывать этот метод поочерёдно у каждого элемента коллекции. При этом мы вообще можем не знать, какой именно инструмент в какой момент нам попадётся, но будет вызвана реализация каждого конкретного инструмента. И это только один из многих вариантов использования полиморфизма как такового. Другой пример (из жизни отчасти, кстати) - это когда вы завязали логику на ArrayList, а потом профилирование показало, что там огромное количество вставок в середины списка, и производительность в итоге сильно проседает под нагрузкой. LinkedList здесь был бы куда лучше, где не нужно было бы делать тучу сдвигов, а просто поменять ссылки. Что в итоге? Если ссылка на объект была изначально как ArrayList, то придётся потом шариться везде в коде и менять все ссылки на LinkedList, а потом молиться, чтобы нигде ничего не грохнулось (а если Вы не один?). А можно было бы объявить ссылку на объект как List, а уже присвоить ему new ArrayList(), и тогда в этой же ситуации достаточно будет поменять new ArrayList() на new LinkedList() в том участке кода, где создаётся этот объект, и вообще не париться, что кто-то там где-то его использует. Реализация разная, но обе структуры являются списками (List), а значит и имеют одинаковые способы взаимодействия, пусть и с разными конкретными реализациями для тех или иных случаев.
Veronika Nikitina17 уровень, Санкт-Петербург
20 мая, 04:36
Спасибо!
Dmitry Sokol17 уровень, Минск
29 марта, 14:15
ничего не ясно амиго врал
Vladislav Polekha28 уровень, Penza
7 марта, 15:20
ищите внизу коментарий левомеколя.
Dmitry19 уровень, Екатеринбург
24 марта, 20:34
Или просто отсортируйте посты по популярности.