JavaRush/Java блог/Java Developer/Расширение и сужение ссылочных типов
Автор
John Selawsky
Senior Java-разработчик и преподаватель в LearningTree

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

Статья из группы Java Developer
участников
Привет! В одной из прошлых лекций мы обсуждали приведение примитивных типов. Давай вкратце вспомним, о чем шла речь. Расширение и сужение ссылочных типов - 1Мы представляли примитивные типы (в данном случае — числовые) в виде матрешек согласно объему памяти, которое они занимают. Как ты помнишь, поместить меньшую матрешку в большую будет просто как в реальной жизни, так и в программировании на 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, которая идет по дереву наследования ниже. Логично, что без явного указания компилятор такую операцию не пропустит, но если в скобках указать тип, все заработает. Расширение и сужение ссылочных типов - 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, ошибка при приведении классов. Давай проговорим еще раз, чтобы было понятнее. Переменная(ссылка)-родитель может указывать на объект класса-потомка:
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();
Переменной-наследнику нельзя присвоить объект предка. Наоборот делать можно.
Комментарии (182)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Mansur
Уровень 30
1 марта, 12:44
Хорошая статья
mkhlv
Уровень 28
20 февраля, 09:33
отлично, а то все несколько смешалось. IDE конечно подсказывает, но лучше понять))
Olga Kuzmins
Уровень 38
10 января, 11:45
Статьи этого автора у меня идут с особым трудом, заметила это еще на 10-11 уровнях Java Syntax. Ну да ладно, все, что нас не убивает, делает нас сильнее)
YUREC
Уровень 36
14 февраля, 10:59
Да, он объясняет довольно простые вещи так широко и подробно что становится немного сложно 😄
Alex
Уровень 28
3 января, 15:04
Статься из серии "Внесём больше путаницы в ваши юные неокрепшие умы"
22 декабря 2023, 12:16
Спасибо, вроде чучуть понял
Максим Li Java Developer
13 ноября 2023, 03:58
Спасибо за статью!
Islam Yunusov
Уровень 31
27 сентября 2023, 08:22
Что касается того, а зачем нам вообще нужен полиморфизм этот, почти нигде не раскрывается: Возьмём, например, класс Cat и класс TomCat. Когда вы создаете объект TomCat и присваиваете его переменной типа Cat, объект TomCat всё равно остается объектом TomCat. Однако, переменная cat, имеющая тип Cat, может видеть и использовать только те методы и свойства, которые объявлены в классе Cat или его родительских классах, если таковые имеются. Если класс Cat и его родители определили какие-то методы или свойства, то объект cat, который имеет тип Cat, сможет использовать их. Но если у класса TomCat есть какие-то специфичные методы или свойства, которые не определены в классе Cat, то переменная cat не сможет непосредственно использовать эти методы или свойства, даже если объект cat на самом деле является объектом TomCat. Пример: class Cat { void makeSound() { System.out.println("Meow"); } } class TomCat extends Cat { void scratchFurniture() { System.out.println("Scratching furniture"); } } public class Main { public static void main(String[] args) { Cat cat = new TomCat(); cat.makeSound(); // Это работает, так как makeSound() определен в Cat // cat.scratchFurniture(); // Это вызовет ошибку компиляции, так как scratchFurniture() не определен в Cat } } Таким образом, объект TomCat, присвоенный переменной типа Cat, может использовать только методы и свойства, определенные в Cat, но не методы, специфичные для TomCat. Это один из аспектов полиморфизма и позволяет вам обеспечивать общий интерфейс для различных подклассов, но сохраняя возможность использования их специфичных функций, если это необходимо.
Islam Yunusov
Уровень 31
27 сентября 2023, 06:02
Кому интересно: Процесс, по которому из значения 10,000,000 в int было получено значение 27,536 в short, связан с различиями в размерности и представлении чисел в двоичной системе. Представление числа 10,000,000 в двоичной системе: 10,000,000 в двоичной системе равно 100110001001011010000000 (32 бита). Приведение int к short: Тип int использует 32 бита для представления чисел. Тип short использует только 16 бит для представления чисел. Сокращение до 16 бит: При явном приведении типа int к short, старшие 16 битов (биты с 17 по 32) отбрасываются. Получение значения в short: Остаются только младшие 16 битов (биты с 1 по 16) числа 100110001001011010000000. Эти 16 битов составляют число 27536 в десятичной системе. Таким образом, из-за отбрасывания старших 16 битов, значение 10000000 в int преобразуется в 27536 в short. Это происходит из-за ограниченной разрядности short, которая позволяет представить только числа в диапазоне от -32,768 до 32,767. В данном случае, число 10,000,000 находится за пределами этого диапазона, и поэтому оно "обрезается" до ближайшего возможного значения в диапазоне short, которое равно 27536.
Denis Gritsay
Уровень 37
22 ноября 2023, 19:52
хм, как то слишком просто поступили создатели Джавы, просто отбросили лишнее и все.
Дмитрий
Уровень 38
Expert
1 декабря 2023, 15:11
Это не создатели Джавы так поступили. Это происходит на уровне процессора и регистров и называется переполнением регистра. Когда мы пытаемся, например, в 2 байтовый регистр АХ положить 4 байтовое число из регистра ЕАХ. Не помещающиеся старшие байты числа в двоичном представлении просто отбросятся и получится вот такая белиберда. Если интересно - вот прекрасное видео, где эта тема раскрывается: https://www.youtube.com/watch?v=k9wK2FThEsk&t=947s
chess.rekrut
Уровень 25
28 августа 2023, 19:13
easy
Valery Java Developer Expert
10 июля 2023, 18:56
Эх, иногда это вопрос того, как удобнне усваивать информацию. Прочитал, понимаю... или нет... ААА... еще прочитал... все также. В итоге, послушал лекцию на Ютубчике и точно все понял.
Павел
Уровень 90
Expert
12 октября 2023, 11:08
У меня через 2 месяца (а может и раньше) можно повторять цикл