jawne rzutowanie z superklasy do podklasy

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;
    }
}

Przypisanie Dog dog = (Dog) animal;nie generuje błędu kompilacji, ale w czasie wykonywania generuje plik ClassCastException. Dlaczego kompilator nie może wykryć tego błędu?

saravanan
źródło
51
TY mówisz kompilatorowi, aby NIE wykrył błędu.
Mauricio

Odpowiedzi:

326

Używając obsady, zasadniczo mówisz kompilatorowi „zaufaj mi. Jestem profesjonalistą, wiem, co robię i wiem, że chociaż nie możesz tego zagwarantować, mówię ci, że ta animalzmienna jest zdecydowanie będzie psem ”.

Ponieważ zwierzę w rzeczywistości nie jest psem (to zwierzę, możesz to zrobić Animal animal = new Dog();i byłby to pies), maszyna wirtualna zgłasza wyjątek w czasie wykonywania, ponieważ naruszyłeś to zaufanie (powiedziałeś kompilatorowi, że wszystko będzie w porządku i jest nie!)

Kompilator jest nieco mądrzejszy niż ślepe akceptowanie wszystkiego, jeśli spróbujesz rzucić obiekty w różnych hierarchiach dziedziczenia (na przykład rzucić psa na łańcuch), kompilator odrzuci to z powrotem, ponieważ wie, że to nigdy nie zadziała.

Ponieważ zasadniczo powstrzymujesz kompilator przed narzekaniem, za każdym razem, gdy rzucasz, ważne jest, aby sprawdzić, czy nie spowodujesz ClassCastExceptionużycia instanceofw instrukcji if (lub czegoś podobnego).

Michael Berry
źródło
Dzięki, ale trzeba psa rozciągają się od Animal jeśli nie, pracę :)
Dosta
66
Uwielbiam to, jak dramatycznie to zabrzmiało
Hendra Anggrian
3
@delive Oczywiście, że tak, ale jak na pytanie, Dog nie rozciągają się od Animal!
Michael Berry
52

Bo teoretycznie Animal animal może to być pies:

Animal animal = new Dog();

Ogólnie rzecz biorąc, poniżanie nie jest dobrym pomysłem. Powinieneś tego unikać. Jeśli go używasz, lepiej dołącz czek:

if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
}
Bozho
źródło
ale poniższy kod generuje błąd kompilacji Dog dog = new Animal (); (niekompatybilne typy). ale w tej sytuacji kompilator identyfikuje Animal jest superklasą, a Dog jest podklasą. Więc przypisanie jest błędne. ale kiedy rzucamy Dog dog = (Dog) animal; akceptuje. proszę wyjaśnij mi o tym
saravanan
3
tak, ponieważ Animal jest superklasą. Nie każde zwierzę jest psem, prawda? Do klas możesz odwoływać się tylko za pomocą ich typów lub nadtypów. Nie ich podtypy.
Bozho
43

Aby uniknąć tego rodzaju ClassCastException, jeśli masz:

class A
class B extends A

Możesz zdefiniować konstruktor w B, który pobierze obiekt A. W ten sposób możemy wykonać "rzutowanie" np:

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
}
Caumons
źródło
24

Opracowanie odpowiedzi udzielonej przez Michaela Berry'ego.

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

Tutaj mówisz do kompilatora „Zaufaj mi. Wiem, że dtak naprawdę odnosi się do Dogobiektu”, chociaż tak nie jest. Pamiętaj, że kompilator jest zmuszony nam zaufać, kiedy robimy upadek .

Kompilator wie tylko o zadeklarowanym typie referencyjnym. JVM w czasie wykonywania wie, czym naprawdę jest obiekt.

Więc kiedy JVM w środowisku wykonawczym zorientuje się, że Dog dfaktycznie odnosi się do obiektu, Animala nie do Dogobiektu, mówi. Hej ... okłamałeś kompilatora i wyrzuciłeś duży tłuszcz ClassCastException.

Więc jeśli jesteś przygnębiony, powinieneś użyć instanceoftestu, aby uniknąć schrzanienia.

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

Teraz przychodzi nam do głowy pytanie. Dlaczego piekielny kompilator pozwala na upadek, skoro w końcu wyrzuci java.lang.ClassCastException?

Odpowiedź jest taka, że ​​wszystko, co może zrobić kompilator, to sprawdzić, czy oba typy są w tym samym drzewie dziedziczenia, więc w zależności od tego, jaki kod mógł pojawić się przed obniżeniem, możliwe animaljest, że jest to typ dog.

Kompilator musi zezwalać na rzeczy, które mogą działać w czasie wykonywania.

Rozważ następujący fragment kodu:

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;
}

Jeśli jednak kompilator jest pewien, że rzutowanie nie będzie możliwe, kompilacja zakończy się niepowodzeniem. IE Jeśli spróbujesz rzutować obiekty w różnych hierarchiach dziedziczenia

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

W przeciwieństwie do downcastingu, upcasting działa niejawnie, ponieważ podczas upcastingu pośrednio ograniczasz liczbę metod, które możesz wywołać, w przeciwieństwie do downcasting, co oznacza, że ​​później możesz chcieć wywołać bardziej szczegółową metodę.

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

Oba powyższe uniesienia będą działać dobrze bez żadnych wyjątków, ponieważ Pies JEST Zwierzęciem, niezależnie od tego, co może zrobić Zwierzę, może to zrobić pies. Ale to nieprawda, odwrotnie.

Zeeshan
źródło
1

Kod generuje błąd kompilacji, ponieważ typ instancji to Animal:

Animal animal=new Animal();

Downcasting nie jest dozwolony w Javie z kilku powodów. Zobacz tutaj po szczegóły.

Simeon
źródło
5
Nie ma błędu kompilacji, to powód jego pytania
Clarence Liu
1

Jak wyjaśniono, nie jest to możliwe. Jeśli chcesz użyć metody z podklasy, oceń możliwość dodania metody do nadklasy (może być pusta) i wywołaj z podklas uzyskując pożądane zachowanie (podklasę) dzięki polimorfizmowi. Więc kiedy wywołasz d.method (), wywołanie powiedzie się bez rzutowania, ale w przypadku, gdy obiekt nie będzie psem, nie będzie problemu

fresko
źródło
0

Aby opracować odpowiedź @Caumons:

Wyobraź sobie, że jedna klasa ojcowska ma wiele dzieci i istnieje potrzeba dodania do tej klasy wspólnego pola. Jeśli weźmiesz pod uwagę wspomniane podejście, powinieneś przejść do każdej klasy podrzędnej jeden po drugim i zmienić ich konstruktory dla nowego pola. dlatego rozwiązanie to nie jest obiecującym rozwiązaniem w tym scenariuszu

Teraz spójrz na to rozwiązanie.

Ojciec może otrzymać własny przedmiot od każdego dziecka. Oto klasa ojców:

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
}

Oto nasza klasa potomna, która powinna implementować jeden ze swoich konstruktorów-ojca, w tym przypadku wspomniany konstruktor:

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);
    }
}

Teraz testujemy aplikację:

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);
    }
}

A oto wynik:

Ojciec Field to: Father String

Pole podrzędne to: Ciąg podrzędny

Teraz możesz łatwo dodawać nowe pola do klasy ojca, nie martwiąc się o kody dzieci do złamania;

Mehdi
źródło