Привіт!
В одній із минулих лекцій ми обговорювали приведення примітивних типів. Давай коротко згадаємо, про що йшлося.
Ми уявляли примітивні типи (у цьому випадку — числові) у вигляді матрьошок згідно обсягу пам'яті, яке вони займають.
Як ти пам’ятаєш, помістити меншу матрьошку в більшу буде просто як у реальному житті, так і у програмуванні на Java.
Розглянемо інший приклад, цікавіший:
Ми уявляли примітивні типи (у цьому випадку — числові) у вигляді матрьошок згідно обсягу пам'яті, яке вони займають.
Як ти пам’ятаєш, помістити меншу матрьошку в більшу буде просто як у реальному житті, так і у програмуванні на 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("я Animal");
}
}
Тварини, як відомо, бувають домашніми і дикими:
public class WildAnimal extends Animal {
public void introduce() {
System.out.println("я WildAnimal");
}
}
public class Pet extends Animal {
public void introduce() {
System.out.println("я Pet");
}
}
Для прикладу візьмемо собачок — домашнього пса і койота:
public class Dog extends Pet {
public void introduce() {
System.out.println("я Dog");
}
}
public class Coyote extends WildAnimal {
public void introduce() {
System.out.println("я Coyote");
}
}
Класи у нас спеціально найпростіші, щоб легше було сприймати їх. Поля нам тут особливо не потрібні, а методу вистачить і одного.
Спробуємо виконати ось такий код:
public class Main {
public static void main(String[] args) {
Animal animal = new Pet();
animal.introduce();
}
}
Як ти думаєш, що буде виведено на консоль? Спрацює метод introduce класу Pet чи класу Animal?
Спробуй обґрунтувати свою відповідь, перш ніж продовжити читання.
А ось і результат!
я 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, яка йде по дереву наслідування нижче. Логічно, що без явного вказання компілятор таку операцію не пропустить, але якщо у дужках вказати тип, все запрацює.
Розглянемо інший приклад, цікавіший:
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();
Змінній-нащадку не можна присвоїти об'єкт предка. Навпаки робити можна.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ