从超类到子类的显式转换

185
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 异常。为什么编译器无法检测到这个错误?


59
你要告诉编译器不要检测错误。 - Mauricio
8个回答

368

使用强制类型转换,你基本上告诉编译器:“相信我。我是个专业人士,我知道自己在做什么,并且尽管你不能保证它,但我告诉你这个animal变量绝对是一只狗。”

由于动物实际上并不是一只狗(它是一个动物,你可以这样写Animal animal = new Dog();,那么它就是一只狗),虚拟机会在运行时抛出异常,因为你违反了这种信任(你告诉编译器一切都没问题,但实际上有问题!)

编译器不会盲目地接受一切,如果你尝试将不同继承层次结构中的对象进行强制转换(例如将狗转换为字符串),那么编译器会将其发回给你,因为它知道那肯定行不通。

因为你实际上只是阻止编译器抱怨,所以每次强制类型转换时都很重要检查你不会通过在if语句中使用instanceof(或类似方法)引起ClassCastException


谢谢,但如果没有将 Dog 扩展自 Animal,则不起作用 :) - user3402040
4
当然,根据问题,“Dog”确实是继承自“Animal”的! - Michael Berry

59

因为从理论上讲,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
可以,因为动物是超类。并不是每个动物都是狗,对吧?你只能通过它们的类型或超类型来引用类,而不是它们的子类型。 - Bozho

56

为了避免这种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
}

31

详细解答了Michael Berry给出的答案。

Dog d = (Dog)Animal; //编译通过但运行时失败

在这里,您告诉编译器“相信我。我知道 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;
}

然而,如果编译器确定强制转换不可能成功,编译将失败。也就是说,如果您尝试在不同的继承层次结构中进行对象转换,则会出现错误。

String s =(String)d; // ERROR:无法将Dog转换为String

与向下转型不同,向上转型是隐式完成的,因为当您进行向上转型时,您隐含地限制了可以调用的方法数量, 与向下转型相反,向下转型意味着以后您可能想调用更特定的方法。

Dog d = new Dog(); Animal animal1 = d; // 没有显式强制转换也能正常工作 Animal animal2 = (Animal) d; // 没有显式强制转换也能正常工作

以上两个向上转型都可以正常工作,没有任何异常,因为狗是一种动物,任何动物能做到的事情,狗都能做到。但反过来则不成立。


3

针对@Caumons的回答进行开发:

假设一个父类有很多子类,并且需要在该类中添加一个公共字段。如果您考虑上述方法,您应该逐个访问每个子类并重构其构造函数以添加新字段。因此,在这种情况下,该解决方案不是一个很有前途的解决方案。

现在看一下这个解决方案。

一个父类可以从每个子类接收一个self对象。这是一个父类:

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

以下是结果:

父字段是:Father String

子字段是:Child String

现在,您可以轻松地向父类添加新字段,而不用担心会破坏您的子类代码。


1

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

Animal animal=new Animal();

在Java中,向下转型是不被允许的,原因有很多。 请参阅此处了解详情。


7
没有编译错误,这就是他提出问题的原因。 - Clarence Liu

1

如前所述,除非您的对象最初是从子类实例化的,否则无法从超类转换为子类。

但是,有一些解决方法。

您只需要一组构造函数和一个方便的方法,该方法将使您的对象转换为Dog,或者返回具有相同Animal属性的新Dog对象。

以下是一个示例,可以做到这一点:

public class Animal {

    public Animal() {}

    public Animal(Animal in) {
        // Assign animal properties
    }

    public Dog toDog() {
        if (this instanceof Dog)
            return (Dog) this;
        return new Dog(this);
    }

}

public class Dog extends Animal {

    public Dog(Animal in) {
        super(in);
    }

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

}

1

如上所述,这是不可能的。 如果您想使用子类的方法,请考虑将该方法添加到超类中(可以为空),并从子类中调用以获得所需的行为(子类),这要归功于多态性。 因此,当您调用d.method()时,调用将成功而无需转换,但是如果对象不是狗,则不会有问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接