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 марта 2024
Хорошая статья
mkhlv Уровень 27
20 февраля 2024
отлично, а то все несколько смешалось. IDE конечно подсказывает, но лучше понять))
Olga Kuzmins Уровень 32
10 января 2024
Статьи этого автора у меня идут с особым трудом, заметила это еще на 10-11 уровнях Java Syntax. Ну да ладно, все, что нас не убивает, делает нас сильнее)
Alex Уровень 28
3 января 2024
Статься из серии "Внесём больше путаницы в ваши юные неокрепшие умы"
22 декабря 2023
Спасибо, вроде чучуть понял
Максим Li Уровень 36
13 ноября 2023
Спасибо за статью!
Islam Yunusov Уровень 31
27 сентября 2023
Что касается того, а зачем нам вообще нужен полиморфизм этот, почти нигде не раскрывается: Возьмём, например, класс 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
Кому интересно: Процесс, по которому из значения 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.
chess.rekrut Уровень 25
28 августа 2023
easy
Valery Уровень 39 Expert
10 июля 2023
Эх, иногда это вопрос того, как удобнне усваивать информацию. Прочитал, понимаю... или нет... ААА... еще прочитал... все также. В итоге, послушал лекцию на Ютубчике и точно все понял.