从父类到子类的显式转换


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
您正在告诉编译器不要检测该错误。
毛里西奥

Answers:


326

通过使用强制转换,您实际上是在告诉编译器“相信我。我是专业人士,我知道我在做什么,我知道尽管不能保证,但是我告诉你这个animal变量肯定是会成为一只狗。”

由于动物实际上不是狗(它是动物,您可以做Animal animal = new Dog();,也可能是狗),因此,VM在运行时会引发异常,因为您违反了这种信任(您告诉编译器一切都会好的,并且不!)

编译器比盲目地接受所有内容要聪明一些,如果您尝试将对象强制转换为不同的继承层次结构(例如,将Dog投射到String),则编译器会将其扔给您,因为它知道这种方法永远无法工作。

因为您实际上只是在阻止编译器抱怨,所以每次转换时,都必须ClassCastException使用instanceofif语句(或类似的方法)来检查不会导致a的错误,这一点很重要。


谢谢,但是如果没有,您需要从Animal那里伸出狗,否则不起作用:)
交付

66
我爱你如何做剧烈它的声音
亨德拉Anggrian

3
@delive当然可以,但是根据问题,Dog 确实从开始Animal
Michael Berry's

52

因为理论上Animal animal 可以是狗:

Animal animal = new Dog();

通常,垂头丧气不是一个好主意。你应该避免它。如果使用它,则最好包括一个检查:

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

但是以下代码生成编译错误Dog dog = new Animal(); (不兼容的类型)。但是在这种情况下,编译器将Animal标识为超类,而Dog为子类。因此分配是错误的。它接受.please向我解释一下
saravanan

3
是的,因为动物是超类。并非每只动物都是狗,对不对?您只能通过类的类型或它们的超类型来引用类。不是他们的子类型。
2011年

43

为了避免这种ClassCastException,如果您有:

class A
class B extends A

您可以在B中定义一个接受A对象的构造函数。通过这种方式,我们可以执行“ cast”,例如:

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

阐述迈克尔·贝瑞(Michael Berry)给出的答案。

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

在这里,您对编译器说的是“请相信我。我知道d这实际上是在指Dog对象”,尽管不是。 请记住,当我们进行向下转换时,编译器被迫信任我们

编译器仅知道声明的引用类型。运行时JVM知道对象的真正含义。

因此,当运行时JVM弄清楚JVM Dog d实际上是指an Animal而不是Dog对象时,它就会说。嘿...您对编译器撒谎并抛出了一个大胖子ClassCastException

因此,如果您垂头丧气,则应该使用instanceof测试来避免搞砸。

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

现在我们想到一个问题。为何地狱编译器最终将抛出时允许向下转换java.lang.ClassCastException

答案是,编译器所能做的就是验证这两种类型是否在同一继承树中,因此根据向下转换之前可能附带的任何代码,该animal类型可能是type 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

与向下转换不同,向上转换是隐式工作的,因为向下转换时隐式限制了您可以调用的方法数量,这与向下转换相反,后者意味着以后可能要调用更具体的方法。

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 Animal(动物可以做,狗可以做)因此毫无例外。但这并不是真的。


1

该代码生成编译错误,因为您的实例类型是动物:

Animal animal=new Animal();

由于多种原因,Java中不允许向下转换。有关详细信息,请参见此处


5
没有编译错误,这是他提出问题的原因
Clarence Liu

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

结果如下:

父亲字段是:父亲弦

子字段是:子字符串

现在,您可以轻松地向父亲班级添加新字段,而不必担心孩子的代码会被破坏;

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.