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

Открыта

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

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

Пример:

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

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

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

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

Код Описание
public static void main(String[] args)
{
Object obj = new Tiger();
Animal animal = (Animal) obj;
Cat cat = (Cat) obj;
Tiger tiger = (Tiger) animal;
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)
{
Animal animal = new Tiger();
doAllAction(animal);

Animal animal2 = new Cat();
doAllAction(animal2);

Animal animal3 = new Animal();
doAllAction(animal3);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Комментарии (134)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий вы должны авторизоваться
Дмитрий14 уровень, Волгоград
вчера, 09:50
Вообще ничего не понял)
Andriy27 уровень, Золочів
23 сентября, 14:21
Чем дальше тем меньше обьяснений что к чему...
Андрей Вайно23 уровень
20 сентября, 11:28
" Тут все довольно просто на самом деле. " Каждый раз, когда она так говорит, я перечитываю гору литературы.
Daniel19 уровень, Таллинн
31 августа, 15:29
Так путает объяснение по движению вверх/вниз по цепочке. Можно же проще - При расширении увеличивается количество доступных методов/переменных, при сужении наоборот. Cat tiger = new Tiger(); // расширение Cat cat = (Cat) cat; // сужение Правильно ведь?
Александр Дудинский16 уровень, Днепр
6 сентября, 20:06
Наоборот, при расширении уменьшается или остается неизменным количество доступных методов, т.к. класс-наследник помимо методов родительского класса может иметь собственные методы.
Rissa16 уровень, Санкт-Петербург
29 августа, 22:46
Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании. А что происходит с переменными класса?
KsArT18 уровень, Харьков
21 сентября, 06:02
"С объектом при таком присваивании ничего не происходит! Меняется только количество методов"
Алексей Боярчук30 уровень, Киев
16 августа, 10:25
Боже, зачем вы паритесь с этими расширениями и сужениями??? Все же проще: Апкастинг - движение вверх по цепочке наследования (когда переменной родителя присваивается объект его наследника) Пример: List list1 = new ArrayList(); Суть: Можем вызывать только те методы, которые есть в List. Если какой то из них переопределен в ArrayList, то вызываться будет именно он, а не метод родителя ------------------------------------------------------------------------------------------------------------------------------------------------- Даункастинг - движение вниз по цепочке наследования (когда переменной наследника присваивается переменная родителя с явным приведением типа) Пример: ArrayList list2 = (ArrayList) list1; Суть: Делаем, если точно знаем, что в list1 находится объект ArrayList (иначе будет ошибка). Теперь можем вызывать все методы, которые есть в ArrayList
Zaur15 уровень
позавчера, 08:20
Суть: Можем вызывать только те методы, которые есть в List. Если какой то из них переопределен в ArrayList, то вызываться будет именно он, а не метод родителя Прошу пояснить этот момент. У нас List (родитель), а ArrayList (наследник). Получается переменной list будут доступны методы, переопределенные в ArrayList'е (если таковые имеются)? Если да, то как это согласуется с данным утверждением из статьи: Например, переменная класса Cat позволяет вызывать методы doAnimalActions & doCatActions, и ничего не знает о методе doTigerActions, даже если ссылается на объект класса Tiger.
Justinian41 уровень, Киев
вчера, 07:24
List интерфейс. Он задает какие методы будут доступны переменным такого типа. ArrayList конкретная реализация, которая содержит реализацию этих методов. То есть тип переменной, можно рассматривать как список методов которые будут доступны для вызова у объекта, на который ссылается переменная. Tiget tiger = new Tiger(); нам доступны все методы, которые есть в классе Tiger; Cat tiger = new Tiger(); нам доступны только те методы, которые описаны в классе Cat. Мы не можем вызывать специфические методы. Например:
class Animal {
 makeSound() {
   System.out.println("rrrrrr");
 }
}
class Human extend Animal {
 makeSound() {
   System.out.println("I'm a Human");
 }
 rideBicycle(){}
 writeJavaProgram(){}
}
class Dog extend Animal() {
 makeSound() {
   System.out.println("wuff-wuff");
 }
wavingTail(){}
}
Если мы сделаем так:
Animal human = new Human();
логично что мы можем только вызвать метод "ИздатьЗвук". Это переменная типа Животное, она ничего не знает о том объекте который лежит в ней. Если бы этого не было. Представь код:
Animal dog = new Dog();
dog.wavingTail();
вроде и ничего. Собака виляет хвостом. Но тогда можно было бы написать и так:
Animal dog = new Human();
dog.wavingTail()
и что тогда было бы? Как человек может вилять хвостом? Нету у него хвоста. Джава строготипизированный язык, чтобы было все четко, есть строгая иерархия типов. Тип определяет список доступных для объекта методов. Можно этот список расширить если сделать приведение типов. Но сама переменная свой тип никогда не поменяет. Животным родилась, животным помрет.
Artur26 уровень
13 июля, 15:51
Эта фраза вводит в заблужение: Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании. Может лучше было написать так: Но при этом теряется возможность вызвать методы, которые не были унаследованы от класса-родителя (этого самого "общего типа"). Ведь при наследовании как раз и добавляются методы родителя (автоматически). Возможно я что-то путаю, поправьте плз.
Сергей Белов18 уровень, Москва
21 июля, 14:31
вот и меня эта фраза запутула, надеюсь ваш вариант верный и тогда всё понятней
Ян19 уровень
24 июля, 07:11
Если в дочерний класс были добавлены новые методы, при расширении их нельзя будет вызвать.
Ваня Петило18 уровень, Львов
11 июля, 21:29
Мне одному немного режет глаза то, как написан первый пример? Я о стиле написания
Vgoose18 уровень, Москва
28 июня, 20:46
Понравилось пояснение про наследование, доступа к методам и размещение класса в памяти. Наследование Дочерний класс в памяти
Павел23 уровень, Москва
18 июня, 19:38
Ничего не понял.. Например, переменная класса Cat позволяет вызывать методы doPetActions & doCatActions, и ничего не знает о методе doTigerActions, даже если ссылается на объект класса Tiger. То есть, авторы имеют ввиду, что если написать что-то из серии
Cat cat = new Tiger(); // Тигр является наследником Cat
То у объекта cat можно вызывать только методы класса Cat? методы класса Тигр работать не будут? Нет, объекту cat доступны все методы класса Tiger. Это можно проверить в Intel JS и убедиться, что методы доступны. Да даже на предыдущем уровне это обсуждали или тут что-то другое имели ввиду?
y-grek15 уровень, Киев
19 июня, 19:52
твой пример (скрин) о переопределении методов.. метод гетНейм(); мы можем вызвать потому, как он есть и у родителя.. а работает он по своему ("я - кит") потому что переопределен в классе дитяти ))) (потомка)
Иван Ващенко18 уровень, Москва
8 июля, 14:38
мне кажется, они имели ввиду уже после неявного расширения типа: Pet pet = cat; в этом случае не будет у него доступа к методам объекта Tiger. Поправьте если не так.
Ваня Петило18 уровень, Львов
11 июля, 21:46
...видимо да, ведь: Движение вверх по цепочке наследования называется расширением, поскольку оно приводит к более общему типу. Но при этом теряется возможность вызвать методы, которые были добавлены в класс при наследовании. Если я правильно понял, то у класса Кот, при создании которого мы ссылались на Тигр, не будет доступа к новым методам, которые есть только у класса Тигр. То есть, будет доступ к своим методам и тем, что были переопределены в классе Тигр. Хотелось бы услышать мнение знающих людей, мы с автором коммента правильно поняли эту часть статьи?
S22 уровень, Минск
26 июля, 20:50
А ты возьми не переопределенный метод от классов - родителей, а создай любой собственный, новый метод в классе Tiger, н-р ownMethod() {System.out.println("I am tiger")}. А потом попробуй вызвать его по ссылке: cat.ownMethod()
Александр Дудинский16 уровень, Днепр
6 сентября, 20:22
Перечень доступных методов определяется типом ссылочной переменной, а конкретная реализация методов определяется типом объекта на который ссылается эта переменная.