явное приведение из суперкласса в подкласс


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;не генерирует ошибку компиляции, но во время выполнения оно генерирует ClassCastException. Почему компилятор не может обнаружить эту ошибку?


51
ВЫ говорите компилятору НЕ обнаруживать ошибку.
Маурисио

Ответы:


326

Используя приведение, вы по существу говорите компилятору: «Поверьте мне. Я профессионал, я знаю, что делаю, и я знаю, что, хотя вы не можете этого гарантировать, я говорю вам, что эта animalпеременная определенно будет собакой. "

Поскольку животное на самом деле не собака (это животное, которое вы могли бы сделать, Animal animal = new Dog();и это была бы собака), виртуальная машина выдает исключение во время выполнения, потому что вы нарушили это доверие (вы сказали компилятору, что все будет хорошо, и это не!)

Компилятор немного умнее, чем просто вслепую принимать все, если вы попытаетесь привести объекты в различные иерархии наследования (например, приведите Dog к String), то компилятор отбросит его назад вам, потому что знает, что это никогда не сработает.

Поскольку вы, по сути, просто не позволяете компилятору жаловаться, каждый раз, когда вы приводите, важно проверять, не вызовете ли вы ClassCastException, используя instanceofоператор if (или что-то в этом роде).


Спасибо, но вам нужно, чтобы собака
вышла

66
Мне нравится, как драматично вы это прозвучали
Хендра Анггриан

3
@ Delive Конечно, вы делаете, но по вопросу, Dog действительно, простирается от Animal!
Майкл Берри

52

Потому что теоретически Animal animal может быть собака

Animal animal = new Dog();

Вообще, уныние не очень хорошая идея. Вам следует избегать этого. Если вы используете его, вам лучше включить проверку:

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

но следующий код генерирует ошибку компиляции Dog dog = new Animal (); (несовместимые типы). но в этой ситуации компилятор определяет, что Animal - это суперкласс, а Dog - это подкласс. Так что присвоение неверно. Но когда мы приводим Dog dog = (Dog) animal; Он принимает. Пожалуйста, объясните мне об этом
saravanan

3
да, потому что Animal это суперкласс. Не каждое животное - Собака, верно? Вы можете ссылаться на классы только по их типам или их супертипам. Не их подтипы.
Божо

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, upcasting работает неявно, потому что, когда вы upcast, вы неявно ограничиваете количество методов, которые вы можете вызывать, в отличие от downcasting, что подразумевает, что позже вы можете захотеть вызвать более конкретный метод.

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

Оба вышеперечисленных upcast будут отлично работать без каких-либо исключений, потому что Dog IS-A Animal может делать то же самое, что и Animal. Но это не правда, наоборот.


1

Код генерирует ошибку компиляции, потому что тип вашего экземпляра - Animal:

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.