1. 引言
多态是面向对象编程的三大支柱之一(与继承和封装并列)。从字面看,该词源自希腊语 “poly”(多)与 “morph”(形)。在编程中,这意味着:一个接口,对应多种实现。
定义
多态是指不同类的对象对同一消息(方法调用)以不同方式作出响应的能力。
也就是说,如果你有一个方法 makeSound(),你可以对任何动物调用它,但猫会喵叫,狗会汪叫,牛会哞叫。对程序员来说,这只是一次 animal.makeSound() 的调用;而实际发生什么——取决于该变量后面真实对象的类型。
生活类比
设想你家里有一个电视遥控器,用它还能控制音箱、投影仪,甚至咖啡机。你按下“开机”——turnOn(),每个设备都会用自己的方式响应。关键在于大家都有“开机”这个“按钮”,但实现各不相同。
Java 示例
class Animal {
void makeSound() {
System.out.println("某种声音...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("汪!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵!");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("哞!");
}
}
现在我们可以这样做:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // 汪!
animal2.makeSound(); // 喵!
animal3.makeSound(); // 哞!
请注意:所有变量的类型都是 Animal,但调用结果取决于对象的实际类型。
2. 多态的类型
在 Java(以及大多数面向对象语言)中,通常区分两种主要的多态:
编译期(静态)多态 — 方法重载(overloading)
即在同一个类中存在多个同名但参数不同的方法。编译器会根据传入的实参来决定调用哪个方法。
示例(稍作预告——细节在下一讲中):
class Printer {
void print(int x) {
System.out.println("数字: " + x);
}
void print(String s) {
System.out.println("字符串: " + s);
}
}
运行期(动态)多态 — 方法重写(overriding)
即在基类中定义一个方法,然后在子类中对其进行重写。究竟调用哪一个方法实现——在程序运行时(runtime)根据对象的实际类型来决定。
示例可参见上面的动物案例。
3. 为什么需要多态?
多态不仅仅是面试中的“漂亮词”。它是让代码更灵活、可扩展、易维护的工具。
代码的通用性
你可以编写只面向基类型的代码,而不用关心具体实现细节。比如,如果你有一个动物列表,你可以遍历它并对每个元素调用 makeSound(),无需关心它是猫还是狗。
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // 每次都会调用“正确”的方法
}
易于扩展
如果明天老板说:“我们加只鹦鹉吧!”,你只需写一个新的类 Parrot extends Animal 并把它加进数组。其余代码无需改动。这就是对扩展开放,对修改关闭(SOLID 中的 OCP 原则)。
简化架构
你可以构建复杂系统,让各部分通过抽象(基类或接口)互相协作,而不用关心具体实现。这能节省时间、精力和咖啡。
4. 关键概念:引用类型与实际类型
变量的引用类型
当你写下 Animal animal = new Dog(); 时,变量 animal 的引用类型是 Animal。也就是说,编译器“认为”它是一个 Animal,只允许调用在 Animal 类中声明的方法。
对象的实际(真实)类型
但内存中实际存放的是 Dog 类型的对象。正是它决定了通过 makeSound() 调用时会执行哪个方法。
示例
Animal animal = new Dog();
animal.makeSound(); // 将调用 Dog.makeSound(),而不是 Animal.makeSound()
重要! 通过基类引用(Animal)无法调用只在 Dog 中存在、且未在基类中声明的方法。
延迟(动态)绑定
这就是运行期发生的“魔法”:当你通过基类引用调用方法时,JVM 会查看对象的实际类型并调用“正确”的实现。这就是多态在起作用。
5. 实用示例:应用中的多态
让我们继续开发教学用的小应用。假设我们在写一个简单的动物园模拟。我们有基类 Animal 以及它的若干子类。我们希望所有动物都能“发出声音”,但又不想为每种动物都写一套独立代码。
步骤 1:基类与子类
class Animal {
void makeSound() {
System.out.println("某种声音...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("汪!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵!");
}
}
步骤 2:动物数组
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
步骤 3:遍历并调用方法
for (Animal animal : zoo) {
animal.makeSound();
}
运行结果:
汪!
喵!
某种声音...
请注意:我们并不知道数组里具体是谁——程序会自行分辨并调用正确的方法。
6. 多态的示意图
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. 另一个例子:真实业务中的多态
比如你在写一个公司人事管理程序。你有一个基类 Employee,以及两个子类:Manager 和 Developer。所有员工都会工作——work(),但工作方式各不相同。
class Employee {
void work() {
System.out.println("员工在工作。");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("经理在开会。");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("开发者在写代码。");
}
}
现在你可以这样做:
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
结果:
经理在开会。
开发者在写代码。
员工在工作。
8. 何时多态不起作用
多态只对基类中声明的方法起作用。如果子类里有自己特有的方法,通过基类引用是看不到的。
class Dog extends Animal {
void fetchStick() {
System.out.println("狗叼回木棍!");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // 编译错误!通过 Animal 看不到该方法
要调用特有方法,需要进行类型转换:
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
但这是另一个话题——要点是:通过多态只能访问在基类中声明的方法。
9. 使用多态时的常见错误
错误 1: 期望通过基类引用能访问子类的所有方法。实际上只能访问基类中声明的方法。
错误 2: 重写方法时不使用 @Override 注解。没有它,方法签名一旦写错就不会发生真正的重写,多态也就失效(基类方法不会被覆盖)。
错误 3: 未进行类型转换就尝试调用子类的特有方法。编译器不允许,因为它不知道基类引用背后具体是什么对象。
错误 4: 将重载(overloading)与重写(overriding)混淆。重载是在同一类中用相同方法名、不同参数列表定义多个方法;重写是在子类中改变基类方法的行为。
GO TO FULL VERSION