Вітання! В одній із минулих лекцій ми обговорювали приведення примітивних типів. Давай коротко згадаємо, про що йшлося.
Ми представляли примітивні типи (у даному випадку – числові) у вигляді матрьошок згідно з обсягом пам'яті, який вони займають. Як ти пам'ятаєш, помістити меншу матрьошку у велику буде просто як у реальному житті, так і у програмуванні на Java.
Розглянемо інший приклад, цікавіше:
![Розширення та звуження посилальних типів - 1](https://cdn.javarush.com/images/article/0ad225b7-3374-435a-a871-6aa25f08cdfa/800.jpeg)
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](https://cdn.javarush.com/images/article/91ff2c96-bc58-4aa7-a33a-6e1fc0d8969b/800.jpeg)
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
і намагаєшся привласнити його змінною 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();
Змінній спадкоємцю не можна присвоїти об'єкт предка. Навпаки робити можна.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ