在学习 Java 面向对象时,我们经常会遇到“向上转型”和“向下转型”这两个概念。很多初学者容易记住结论,却不理解为什么:为什么子类对象可以自动转成父类?为什么父类对象转成子类时必须手动强转?为什么有时候强转会抛出 ClassCastException?为什么又要用 instanceof 判断?

本文就围绕这些问题,结合 AnimalDog 的例子,把 Java 中的向下转型讲清楚。

一、先理解父类和子类的关系

假设我们有一个父类 Animal,还有一个子类 Dog

class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("狗叫");
    }
}

这里 Dog extends Animal,表示 Dog 继承了 Animal

也就是说:

Dog 是 Animal

狗一定是动物,所以 Dog 对象天然具备 Animal 的特征。它既可以调用父类中的 eat() 方法,也可以调用自己特有的 bark() 方法。

但是反过来就不一定成立:

Animal 不一定是 Dog

动物可能是狗,也可能是猫、鸟、鱼,甚至只是一个普通的 Animal 对象。所以父类和子类之间虽然有继承关系,但不是可以随便互相转换的。

二、什么是向上转型?

向上转型指的是:把子类对象赋值给父类引用。

例如:

Dog dog = new Dog();
Animal animal = dog;

也可以直接写成:

Animal animal = new Dog();

这就是向上转型。

为什么这个过程是安全的?

因为狗一定是动物。DogAnimal 的子类,所以一个 Dog 对象肯定可以当作 Animal 来使用。

因此 Java 允许自动完成向上转型,不需要手动加括号。

Animal animal = new Dog(); // 自动向上转型

这句话的重点是:

引用类型是 Animal
真实对象是 Dog

也就是说,变量 animal 的编译类型是 Animal,但它实际指向的对象是 Dog

这就是 Java 多态的基础。我们可以用父类引用指向子类对象,从而提高代码的扩展性。

三、什么是向下转型?

向下转型指的是:把父类引用重新转换成子类引用。

例如:

Animal animal = new Dog();

Dog dog = (Dog) animal;
dog.bark();

这里 animal 的声明类型是 Animal,但是它真实指向的是一个 Dog 对象。

所以我们可以把它强制转换成 Dog

Dog dog = (Dog) animal;

这样转换之后,dog 就可以调用 Dog 类中特有的方法:

dog.bark();

因为 bark()Dog 中的方法,父类 Animal 中没有这个方法。如果不进行向下转型,直接写:

animal.bark();

编译器会报错。因为在编译器看来,animalAnimal 类型,而 Animal 类中没有 bark() 方法。

所以向下转型的一个常见作用就是:当父类引用实际指向的是子类对象时,通过强制转换恢复成子类类型,从而调用子类特有的方法。

四、为什么向下转型需要手动强转?

向上转型是安全的,因为子类一定属于父类。

例如:

Dog dog = new Dog();
Animal animal = dog;

这没有风险,因为狗一定是动物。

但是向下转型不同:

Animal animal = new Animal();
Dog dog = (Dog) animal;

这就有问题了。

因为 animal 指向的真实对象是:

new Animal()

它只是一个普通的 Animal 对象,不是 Dog 对象。

你现在强行把它转成 Dog

Dog dog = (Dog) animal;

相当于告诉 Java:

请把这个普通动物当成狗来用。

但是 Java 在运行时会检查这个对象的真实类型,发现它其实不是 Dog,于是就会抛出异常:

ClassCastException

完整代码如下:

class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("狗叫");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Animal();

        Dog dog = (Dog) animal; // 运行时报错:ClassCastException

        dog.bark();
    }
}

这段代码编译阶段可能不会报错,但是运行阶段会报错。原因是 Java 编译器只能看到你写了强制类型转换,它认为你已经明确告诉它“我要这样转”。但是到了运行时,JVM 会检查对象的真实类型,发现这个对象并不是 Dog,所以就会抛出异常。

五、变量类型和真实对象类型不是一回事

理解向下转型时,最重要的是区分两个概念:

变量的声明类型
对象的真实类型

看下面两段代码:

Animal animal1 = new Dog();
Animal animal2 = new Animal();

它们左边的声明类型都是 Animal,但是右边创建出来的真实对象不一样。

第一句:

Animal animal1 = new Dog();

表示:

animal1 的声明类型是 Animal
animal1 指向的真实对象是 Dog

第二句:

Animal animal2 = new Animal();

表示:

animal2 的声明类型是 Animal
animal2 指向的真实对象是 Animal

所以:

Dog dog1 = (Dog) animal1; // 可以
Dog dog2 = (Dog) animal2; // 报错

为什么第一个可以?

因为 animal1 实际上指向的就是 Dog 对象,只是之前被当成 Animal 来看。现在把它转回 Dog,是合理的。

为什么第二个不可以?

因为 animal2 指向的是一个真正的 Animal 对象,它本来就不是 Dog。你不能把一个普通动物强行变成狗。

所以向下转型能不能成功,关键不看左边变量声明成什么类型,而是看对象在运行时的真实类型是什么。

六、ClassCastException 是什么?

ClassCastException 的中文意思是:类型转换异常。

它通常出现在错误的强制类型转换中。

例如:

Animal animal = new Animal();
Dog dog = (Dog) animal;

这里 animal 的真实类型是 Animal,不是 Dog,所以强转失败。

再比如:

Animal animal = new Cat();
Dog dog = (Dog) animal;

如果 CatDog 都继承自 Animal,这段代码也会报错。

因为虽然 CatDog 都是动物,但是猫不是狗。一个 Cat 对象不能被强制转换成 Dog 对象。

代码示例:

class Animal {}

class Dog extends Animal {}

class Cat extends Animal {}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Cat();

        Dog dog = (Dog) animal; // 运行时报错:ClassCastException
    }
}

这也是很多初学者容易犯的错误:以为只要两个类有同一个父类,就可以互相转换。实际上不可以。兄弟类之间不能直接强转。

七、为什么 Java 能知道真实类型?

因为 Java 的对象在运行时会保留自己的类型信息。

当你写:

Animal animal = new Dog();

虽然变量 animal 的声明类型是 Animal,但是堆内存中真实创建出来的对象仍然是 Dog

Java 并不会因为你用 Animal 类型来接收它,就真的把它变成 Animal 对象。

它只是被“当成 Animal 来使用”,但它的真实身份还是 Dog

所以当你写:

Dog dog = (Dog) animal;

JVM 会在运行时检查:

animal 指向的真实对象是不是 Dog?

如果是,就转换成功。

如果不是,就抛出 ClassCastException

这就是为什么向下转型的风险发生在运行阶段,而不是一定发生在编译阶段。

八、如何避免 ClassCastException?

解决方式就是在向下转型之前,先使用 instanceof 进行判断。

instanceof 的作用是:判断一个对象是否是某个类或其子类的实例。

例如:

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

这段代码的意思是:

如果 animal 指向的真实对象是 Dog 类型,再进行强制类型转换。

完整示例:

class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("狗叫");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();

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

输出结果:

狗叫

如果改成:

Animal animal = new Animal();

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

这时 animal instanceof Dog 的结果是 false,所以不会进入 if 代码块,也就不会发生强制类型转换异常。

九、Java 新版本中 instanceof 的简化写法

在较新的 Java 版本中,可以使用模式匹配写法,让代码更简洁。

传统写法:

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

新写法:

if (animal instanceof Dog dog) {
    dog.bark();
}

这句话的意思是:

如果 animal 是 Dog 类型,就自动转换成 Dog,并命名为 dog。

这样就不用再手动写:

Dog dog = (Dog) animal;

代码更简洁,也更安全。

十、向下转型常见使用场景

向下转型通常出现在多态场景中。

例如我们有一个方法,参数类型是父类:

public static void handleAnimal(Animal animal) {
    animal.eat();

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

调用时可以传入不同的子类对象:

handleAnimal(new Dog());
handleAnimal(new Cat());

这样方法参数使用父类类型,可以接收更多种对象,提高代码灵活性。

但是如果想调用某个子类特有的方法,比如 Dogbark(),就需要先判断它是不是 Dog,然后再向下转型。

不过从面向对象设计角度来说,如果一个地方频繁使用 instanceof 和强制类型转换,有时也说明代码设计可能可以优化。比如可以考虑把公共行为抽象到父类方法中,通过方法重写实现多态,而不是到处判断具体类型。

例如:

class Animal {
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("狗叫");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("猫叫");
    }
}

然后直接调用:

Animal animal = new Dog();
animal.makeSound();

输出:

狗叫

这样就不需要强制转换了,这才是多态更推荐的用法。

十一、总结

Java 中的向下转型,本质上是把父类引用转换成子类引用。它之所以需要手动进行,是因为这种转换并不一定安全。

向上转型是安全的,因为子类一定是父类。例如,狗一定是动物,所以 Dog 可以自动转成 Animal

但是向下转型有风险,因为父类不一定是某个具体子类。例如,动物不一定是狗,所以 Animal 不能随便强转成 Dog

当代码写成:

Animal animal = new Animal();
Dog dog = (Dog) animal;

程序运行时会抛出 ClassCastException。原因是 animal 指向的真实对象是 Animal,并不是 Dog。Java 在运行时会检查对象的真实类型,如果发现对象实际类型和目标类型不兼容,就会抛出类型转换异常。

正确做法是在向下转型前使用 instanceof 判断:

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

这样只有在确认 animal 真实指向的是 Dog 对象时,才会进行强制类型转换,从而避免运行时异常。

最后记住一句话:

父类引用可以指向子类对象,但父类对象不能随便强转成子类对象。

也可以再简化成一句面试回答:

向上转型是子类转父类,安全,可以自动完成;向下转型是父类转子类,需要强制转换。由于父类引用指向的真实对象不一定是目标子类,所以可能出现 ClassCastException。为了避免异常,转型前应该使用 instanceof 判断对象的真实类型。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐