是什么?
“父类引用指向子类对象”指的是,你可以创建一个子类的实例,但是用一个声明为父类类型的引用变量来指向它。
语法格式:
// 父类类型 引用变量名 = new 子类构造器(); Parent p = new Child();
举个例子:
假设我们有一个父类 Animal 和一个子类 Dog。
// 父类
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
// 子类
class Dog extends Animal {
// 子类继承了父类的 eat() 方法
// 子类可以添加自己的新方法
public void bark() {
System.out.println("汪汪叫");
}
}
我们就可以使用父类引用指向子类对象:
// 父类引用 p 指向子类 Dog 的实例 Animal p = new Dog();
这里,p 这个变量的编译时类型(或称为静态类型)是 Animal,而它的运行时类型(或称为动态类型)是 Dog。
为什么这么做?—— 两大核心优势
这种做法主要有两个非常重要的目的:
实现多态
这是最主要的原因,多态的字面意思是“多种形态”,在 Java 中,它意味着“一个接口,多种实现”。
通过父类引用指向子类对象,我们可以用统一的方式来处理不同子类的对象,从而写出更灵活、可扩展性更强的代码。
示例:
假设我们有一个动物园 Zoo 类,它管理各种动物,我们可以将动物数组声明为 Animal[],这样无论是 Dog、Cat 还是 Bird,都可以被统一管理。
// 子类 Cat
class Cat extends Animal {
@Override
public void eat() {
System.out.println("小猫吃鱼");
}
}
// 子类 Bird
class Bird extends Animal {
@Override
public void eat() {
System.out.println("小鸟吃虫子");
}
}
// Zoo 类
class Zoo {
public void feedAnimals(Animal[] animals) {
for (Animal animal : animals) {
// 统一调用 eat() 方法
animal.eat();
}
}
}
// 主程序
public class Main {
public static void main(String[] args) {
// 创建一个动物园,里面可以容纳各种动物
Animal[] animals = new Animal[3];
animals[0] = new Dog(); // 父类引用指向子类对象
animals[1] = new Cat(); // 父类引用指向子类对象
animals[2] = new Bird(); // 父类引用指向子类对象
Zoo zoo = new Zoo();
zoo.feedAnimals(animals);
}
}
输出结果:
动物吃东西
动物吃东西
动物吃东西
等等,输出好像不对!这是因为我们的 Dog, Cat, Bird 类中的 eat() 方法没有被 @Override 注解正确重写,让我们修正一下:
// Dog 类
class Dog extends Animal {
@Override // 关键字,表示重写父类方法
public void eat() {
System.out.println("小狗吃骨头");
}
// ... bark() 方法
}
// Cat 类
class Cat extends Animal {
@Override
public void eat() {
System.out.println("小猫吃鱼");
}
}
// Bird 类
class Bird extends Animal {
@Override
public void eat() {
System.out.println("小鸟吃虫子");
}
}
再次运行 Main 程序,输出结果为:
小狗吃骨头
小猫吃鱼
小鸟吃虫子
这才是多态的正确体现!feedAnimals 方法内部代码完全不需要修改,我们只需要传入新的子类(Tiger)对象,它就能自动调用 Tiger 自己的 eat() 方法,这就是可扩展性。
解耦和提高代码的可维护性
通过依赖父类(接口或抽象类)而不是具体的子类,代码的各个模块之间的耦合度降低了。Zoo 类只关心 Animal 这个抽象概念,而不关心具体是 Dog 还是 Cat,这使得未来增加新的动物种类时,不需要修改 Zoo 类的任何代码,符合“开闭原则”(对扩展开放,对修改关闭)。
规则与限制:能做什么,不能做什么?
当一个父类引用指向子类对象时,能调用哪些方法,取决于引用的编译时类型,而不是对象的运行时类型。
能做什么?(访问父类的成员)
你可以通过父类引用访问子类从父类继承来的成员(包括方法和属性),或者子类重写了的方法。
Animal p = new Dog(); // 1. 调用继承来的方法 p.eat(); // 正确!Dog 类继承并重写了 eat(),这里调用的是 Dog 的 eat() // 2. 调用重写的方法 // 这和调用继承来的方法在效果上是一样的,因为重写就是覆盖父类的实现 p.eat(); // 输出 "小狗吃骨头"
不能做什么?(访问子类特有的成员)
你不能通过父类引用直接调用子类自己新增的、父类没有的方法,因为在编译器看来,p 是一个 Animal 对象,而 Animal 类里没有 bark() 方法,所以编译会报错。
Animal p = new Dog(); // p.bark(); // 编译错误! // 提示信息:cannot find symbol method bark()
为什么编译器要这么设计?
这保证了类型安全,编译器只允许你调用它在“类型契约”中承诺的方法,如果你不确定一个 Animal 引用指向的具体对象是否有 bark() 方法,那么就不应该允许你调用它,否则可能导致运行时错误。
如何访问子类特有的成员?(向下转型)
如果你确定父类引用指向的是一个特定子类的对象,并且你需要调用子类特有的方法,你可以进行向下转型。
Animal p = new Dog(); // 1. 先进行向下转型 Dog d = (Dog) p; // 2. 然后就可以调用子类特有的方法了 d.bark(); // 输出 "汪汪叫" // 也可以链式调用 ((Dog) p).bark(); // 直接转型并调用
⚠️ 重要:向下转型的风险
向下转型是有风险的,如果你转型的类型和对象的实际类型不匹配,会抛出 ClassCastException。
Animal p = new Dog(); // p 的实际类型是 Dog // 错误的转型:试图将 Dog 对象转为 Cat // Cat c = (Cat) p; // 运行时抛出 ClassCastException
如何安全地向下转型?
使用 instanceof 操作符进行判断,这是一种非常安全的做法。
Animal p = new Dog();
if (p instanceof Dog) {
// 只有在 p 确实是 Dog 的实例时,才进行转型
Dog d = (Dog) p;
d.bark(); // 安全执行
} else {
System.out.println("这个对象不是 Dog,不能调用 bark() 方法。");
}
| 特性 | 描述 |
|---|---|
| 核心语法 | Parent p = new Child(); |
| 编译时类型 | 由声明变量时使用的类型决定(这里是 Parent),决定了编译器允许你调用哪些方法。 |
| 运行时类型 | 由 new 关键字创建的对象的实际类型决定(这里是 Child),决定了方法调用的具体实现。 |
| 主要优点 | 实现多态:可以用统一接口处理不同子类对象,提高代码灵活性。 解耦:降低模块间依赖,提高可维护性和可扩展性。 |
| 访问规则 | - 可以访问:子类继承的成员,以及子类重写的方法。 - 不可以访问:子类新增的、父类没有的成员。 |
| 访问子类特有成员 | 需要进行向下转型,并最好使用 instanceof 进行安全检查,以避免 ClassCastException。 |
理解“父类引用指向子类对象”是迈向 Java 高级编程的必经之路,它直接关系到你是否能熟练运用多态和面向接口编程等核心设计思想。
