явний кастинг від суперкласу до підкласу


161
public class Animal {
    public void eat() {}
}

public class Dog extends Animal {
    public void eat() {}

    public void main(String[] args) {
        Animal animal = new Animal();
        Dog dog = (Dog) animal;
    }
}

Призначення Dog dog = (Dog) animal;не створює помилки компіляції, але під час виконання воно генерує a ClassCastException. Чому компілятор не може виявити цю помилку?


51
ВИ говорите компілятору НЕ виявляти помилку.
Маурісіо

Відповіді:


326

Використовуючи акторський склад, ви по суті говорите компілятору "довіряйте мені. Я професіонал, я знаю, що я роблю, і я знаю, що хоча ви не можете цього гарантувати, я кажу вам, що ця animalзмінна, безумовно, буде собакою ».

Оскільки тварина насправді не собака (це тварина, ви могли б це зробити, Animal animal = new Dog();і це була б собака), ВМ викидає виняток під час виконання, тому що ви порушили цю довіру (ви сказали компілятору, що все буде гаразд, і це ні!)

Компілятор трохи розумніший, ніж просто сліпо прийняти все, якщо ви спробуєте передати об'єкти в різних ієрархіях спадкування (наприклад, киньте собаку на рядок), тоді компілятор відкине її назад вам, бо знає, що ніколи не може працювати.

Оскільки ви по суті просто зупиняєте компілятор від скарги, кожен раз, коли ви передаєте, важливо перевірити, чи не викликаєте ви ClassCastException, використовуючи instanceofв операторі if (або щось для цього.)


Дякую, але вам потрібно продовжити собаку від Animal, якщо ні, не працювати :)
доставляйте

66
Мені подобається, як драматично ти прозвучав
Хендра Ангдріан

3
@delive Звичайно ви робите, але , як на питання, Dog чи дійсно сягати від Animal!
Майкл Беррі

52

Тому що теоретично собака Animal animal може бути:

Animal animal = new Dog();

Взагалі, зрив з каналів не є хорошою ідеєю. Вам слід уникати цього. Якщо ви користуєтесь ним, краще включіть чек:

if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
}

але наступний код створює помилку компіляції Dog dog = new Animal (); (несумісні типи). Але в цій ситуації компілятор ідентифікує Тварина - це суперклас, а Собака - підклас. Тому присвоєння неправильне. він приймає. будь ласка, поясніть мені про це
saravanan

3
так, тому що тварина - це суперклас. Не кожна тварина - Собака, правда? Ви можете посилатися на класи лише за їх типами або за їх супертипами. Не їх підтипи.
Божо

43

Щоб уникнути подібного роду ClassCastException, якщо у вас є:

class A
class B extends A

Ви можете визначити конструктор у B, який приймає об’єкт A. Таким чином ми можемо зробити "кидок", наприклад:

public B(A a) {
    super(a.arg1, a.arg2); //arg1 and arg2 must be, at least, protected in class A
    // If B class has more attributes, then you would initilize them here
}

24

Розвиток відповіді, даної Майклом Беррі.

Dog d = (Dog)Animal; //Compiles but fails at runtime

Ось ви говорите компілятору "Повір мені. Я знаю d, що справді йдеться про Dogоб'єкт", хоча це не так. Пам'ятайте, що компілятор змушений нам довіряти, коли ми робимо спад .

Компілятор знає лише про заявлений тип посилання. JVM під час виконання роботи знає, що є об'єктом насправді.

Отже, коли JVM під час виконання з'ясовує, що Dog dсправді йдеться про об'єкт, який він говорить, Animalа не Dogоб'єкт. Гей ... ти брешеш компілятору і кидаєш великий жир ClassCastException.

Тож якщо ви перебуваєте на каналі, вам слід скористатися instanceofтестом, щоб уникнути викручування.

if (animal instanceof Dog) { Dog dog = (Dog) animal; }

Тепер питання нам приходить в голову. Чому чортовий компілятор дозволяє скинути, коли врешті-решт він збирається кинути java.lang.ClassCastException?

Відповідь полягає в тому, що все, що може зробити компілятор, - це перевірити, чи є два типи в одному дереві спадкування, тож залежно від того, який код міг би бути до занепаду, можливо, він animalмає тип dog.

Компілятор повинен дозволити речі, які могли б працювати під час виконання.

Розглянемо наступний фрагмент коду:

public static void main(String[] args) 
{   
    Dog d = getMeAnAnimal();// ERROR: Type mismatch: cannot convert Animal to Dog
    Dog d = (Dog)getMeAnAnimal(); // Downcast works fine. No ClassCastException :)
    d.eat();

}

private static Animal getMeAnAnimal()
{
    Animal animal = new Dog();
    return animal;
}

Однак, якщо компілятор впевнений, що передача не змогла б працювати, компіляція не вдасться. IE Якщо ви намагаєтеся викидати об'єкти в різні ієрархії спадкування

String s = (String)d; // ERROR : cannot cast for Dog to String

На відміну від downcasting, оновлення працює неявно, тому що, коли ви поновлюєтеся, ви неявно обмежуєте кількість методів, які ви можете викликати, як протилежне до downcasting, що означає, що пізніше ви можете захотіти використовувати більш конкретний метод.

Dog d = new Dog(); Animal animal1 = d; // Works fine with no explicit cast Animal animal2 = (Animal) d; // Works fine with n explicit cast

Обидва вищезгадані події будуть справно працювати без будь-якого винятку, тому що собака IS-A тварина, яка може робити аніт тварини, може робити і собака. Але це неправда vica навпаки.


1

Код створює помилку компіляції, оскільки тип вашого примірника - тварина:

Animal animal=new Animal();

Знищення рівня передачі даних на Java не допускається з кількох причин. Детальніше дивіться тут .


5
Помилки компіляції немає, це причина його запитання
Кларенс Лю

1

Як пояснили, це неможливо. Якщо ви хочете використовувати метод підкласу, оцініть можливість додати метод до надкласу (може бути порожнім) та зателефонуйте з підкласів, отримуючи потрібну поведінку (підклас) завдяки поліморфізму. Тож, коли ви зателефонуєте на d.method (), виклик буде успішним без проходження кастингу, але якщо об'єкт не буде собакою, не виникне проблем


0

Щоб розробити відповідь @Caumons:

Уявіть, що один клас батьків має багато дітей, і є необхідність додати до цього класу загальне поле. Якщо ви зважаєте на згаданий підхід, вам слід переходити до кожного класу дітей по одному та переробляти їх конструктори для нового поля. отже, це рішення не є перспективним рішенням у цьому сценарії

Тепер погляньте на це рішення.

Батько може отримати самооб'єкт від кожної дитини. Ось клас батька:

public class Father {

    protected String fatherField;

    public Father(Father a){
        fatherField = a.fatherField;
    }

    //Second constructor
    public Father(String fatherField){
        this.fatherField = fatherField;
    }

    //.... Other constructors + Getters and Setters for the Fields
}

Ось наш дочірній клас, який повинен реалізувати один із конструкторів свого батька, в даному випадку вищезгаданий конструктор:

public class Child extends Father {

    protected String childField;

    public Child(Father father, String childField ) {
        super(father);
        this.childField = childField;
    }

    //.... Other constructors + Getters and Setters for the Fields

    @Override
    public String toString() {
        return String.format("Father Field is: %s\nChild Field is: %s", fatherField, childField);
    }
}

Тепер ми тестуємо додаток:

public class Test {
    public static void main(String[] args) {
        Father fatherObj = new Father("Father String");
        Child child = new Child(fatherObj, "Child String");
        System.out.println(child);
    }
}

І ось результат:

Батько Поле: Батько Струна

Поле дитини - це: дитяча струна

Тепер ви можете легко додавати нові поля до класу батька, не турбуючись про те, що ваші коди діють, щоб зламати;

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.