- 0
- 302 words
抽象类学完了,你现在应该知道 「父类只声明、子类去实现」 是一种什么感觉了。那么继续想一个问题:
假如你写了一个方法,参数类型是某个父类——比如 Animal animal——然后你传入 Dog、Cat、Bird 三个不同的子类对象。这个方法调用 animal.makeSound(),结果会怎样?
狗汪汪叫、猫喵喵叫、鸟叽叽叫。 同一个方法签名,传不同的对象,表现出不同的行为。
这就是 多态(Polymorphism)——面向对象三大特性中的最后一张拼图。它的字面意思是「多种形态」:同一个行为,在不同对象上有不同的表现。
封装关注「能不能看到」,继承关注「能不能复用」,多态关注「能不能灵活替换」。
一、多态的底层基石
在拆解多态的语法之前,我们必须先搞清楚一件事:Java 是怎么决定「调用哪个方法」的。
1.1 编译时类型 vs 运行时类型
看一段代码:
Animal a = new Dog();
这里 a 有两个身份:
- 编译时类型(声明类型):
Animal——编译器只看这个 - 运行时类型(实际类型):
Dog——JVM 运行时才真正知道
多态的精髓就在于:编译看左边,运行看右边。
编译器检查 a 有没有某个方法,只看 Animal 类里定义了什么。但真正调用的时候,JVM 看的是 Dog 类有没有重写这个方法——有就调用 Dog 的,没有才往上找父类的。
1.2 动态绑定(Dynamic Binding)
上面这个「编译时不知道、运行时才决定」的机制,就叫 动态绑定,也叫 虚方法调用(Virtual Method Invocation)。
方法调用的绑定有两种:
| 绑定类型 | 什么时候决定 | 针对什么 |
|---|---|---|
| 静态绑定 | 编译期 | static 方法、private 方法、final 方法、构造器 |
| 动态绑定 | 运行期 | 普通实例方法(非 static / private / final) |
静态绑定的方法在编译时就确定了调用目标——比如 static 方法跟类绑定,不存在多态行为。final 方法不能被重写,JVM 也知道可以直接锁定。只有那些能被重写的普通方法,JVM 才需要「动态绑定」——运行时看对象的实际类型,找到对应的方法去执行。
1.3 一个小实验看看动态绑定怎么工作
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
public static void staticMethod() {
System.out.println("Animal 的静态方法");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
public static void staticMethod() {
System.out.println("Dog 的静态方法");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
a.makeSound(); // 动态绑定 → 输出:汪汪汪!
a.staticMethod(); // 静态绑定 → 输出:Animal 的静态方法
}
}
看到了吗?同样的 a.staticMethod(),如果按直觉以为也是多态,那就踩坑了——静态方法没有多态行为,调用的是声明类型的版本,而不是实际类型的。
反过来说,普通方法 makeSound() 是动态绑定的,所以虽然 a 声明为 Animal 类型,但实际调用的是 Dog 的版本。这就是多态。
二、向上转型
多态的前提是 向上转型(Upcasting)——把子类对象赋值给父类引用。这是 Java 中唯一一个「不需要强转」的自动类型转换。
Dog dog = new Dog();
Animal animal = dog; // 向上转型,自动完成
Animal another = new Dog(); // 常见写法,也是向上转型
// 父类引用可以调用子类继承或重写的方法
animal.makeSound(); // 动态绑定 → 汪汪汪!
animal.eat(); // 子类继承的方法也能调
2.1 向上转型的约束
向上转型有一个 限制:通过父类引用只能调用父类里定义过的方法,子类新增的方法是看不到的。
class Dog extends Animal {
public void wagTail() {
System.out.println("摇尾巴");
}
}
Animal a = new Dog();
a.makeSound(); // ✅ 父类有 makeSound(),可以调
a.wagTail(); // ❌ 编译错误!Animal 没有 wagTail() 方法
这个限制看似麻烦,其实恰好多态存在的意义——如果你可以通过父类引用调用子类新增的方法,那多态就失去了它的设计价值。多态想要的是「一套统一的 API 调用接口」,而不是「每个子类都有自己的一堆奇葩方法」。
三、向下转型与 instanceof
向上转型是自动的,但有时候我们需要反向操作——把父类引用变回子类引用,去调用子类特有的方法。这个过程叫 向下转型(Downcasting),需要 强制类型转换。
Animal a = new Dog();
Dog dog = (Dog) a; // 向下转型,需要强制转换
dog.wagTail(); // ✅ 现在可以调子类特有的方法了
3.1 类型转换的错误——ClassCastException
向下转型是有风险的。如果引用的真实类型不是目标子类,运行时会抛出 ClassCastException:
Animal a = new Cat();
Dog dog = (Dog) a; // ❌ 运行时异常!Cat 不能转为 Dog
3.2 安全的向下转型:instanceof
Java 提供了 instanceof 关键字来判断一个对象的真实类型,避免转型爆炸:
if (a instanceof Dog) {
Dog dog = (Dog) a;
dog.wagTail();
} else if (a instanceof Cat) {
Cat cat = (Cat) a;
cat.climbTree();
}
从 Java 14 开始,还可以用 instanceof 模式匹配的语法,把类型判断和转型写在一起:
if (a instanceof Dog dog) {
dog.wagTail(); // 直接使用 dog 变量,自动转型
}
这个写法更简洁,推荐在 JDK 14+ 的项目里使用。
3.3 使用小建议
- 向上转型:放心用,自动、安全、是日常开发的主要操作
- 向下转型:能少用就少用。频繁向下转型说明你的设计可能有问题——多态的目的就是避免判断具体类型
- instanceof:如果一定要向下转型,务必先用 instanceof 检查
四、多态的三大体现形式
多态不只是「父类引用指向子类对象」这一种形式。在 Java 中,多态体现在三个方面:
4.1 编译时多态——方法重载(Overload)
方法重载是指同一个类里有多个同名方法,参数列表不同。虽然它也叫「多态」,但它其实是 编译时多态——编译器在编译时就确定了调用哪个版本的方法。
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
Calculator calc = new Calculator();
calc.add(1, 2); // 编译时就确定调用 int 版本
calc.add(1.0, 2.0); // 编译时就确定调用 double 版本
方法重载的「多态」发生在编译期,通过参数的静态类型来决定。好处是让同一个方法名支持多种参数,代码更直观。
4.2 运行时多态——方法重写(Override)
这才是我们通常说的「多态」——子类重写父类方法,JVM 在运行期通过动态绑定决定调用哪个版本。这是面向对象设计的核心武器。
4.3 参数多态——泛型(Generics)
严格来说泛型不算经典的 OOP 多态,但广义上它也属于「一个接口适配多种类型」。泛型我们会在集合框架与泛型部分详细讲解,这里先提个名,有个印象就行。
五、多态与变量
在 Java 中,方法有多态,但变量没有多态。 这是一个容易踩的坑。
class Parent {
public String name = "父类变量";
public void show() {
System.out.println("父类方法");
}
}
class Child extends Parent {
public String name = "子类变量"; // 不是重写,是隐藏
@Override
public void show() {
System.out.println("子类方法");
}
}
public class Test {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.name); // 输出:父类变量(变量没有多态)
p.show(); // 输出:子类方法(方法有多态)
}
}
同样用父类引用指向子类对象,访问变量看声明类型(编译时类型),调用方法看实际类型(运行时类型,动态绑定非静态/私有/final 的方法)。
所以如果你的子类和父类有同名字段,永远别这么做。这会让代码的读者一头雾水,觉得代码像个薛定谔的猫——看起来像是父类的值,实际又像是子类的值。字段应该是私有的,通过 getter/setter 访问,就不会有这种问题。
六、多态中隐藏的细节
6.1 private 方法没有多态
子类不能重写父类的 private 方法。即使你在子类里写了一个同名的 private 方法,那也是子类自己的方法,跟父类没有任何关系——没有 override,只是恰好同名。
6.2 static 方法也没有多态
前面已经用 staticMethod() 演示过了。静态方法属于类,不在对象的方法表里,JVM 通过声明类型直接调用,不存在动态绑定。
6.3 final 方法不能被重写
final 方法明确不允许子类重写,所以也不存在多态。JVM 可以直接静态绑定。
6.4 构造器没有多态
构造器不能被继承,也就不存在重写。父类和子类的构造器是独立的,只是通过 super() 形成调用链——这是继承的机制,不是多态。
七、总结
这一篇我们讲了多态最核心的底层机制:
- 多态的本质:同一方法签名,不同对象不同行为
- 底层原理:编译时类型 vs 运行时类型、动态绑定(虚方法调用)
- 向上转型:子类对象→父类引用,自动安全,但只能调父类有的方法
- 向下转型与 instanceof:父类引用→子类引用,需要强制转换,先用 instanceof 检验
- 三种多态:重载(编译时)、重写(运行时)、泛型(参数多态)
- 变量没有多态:字段访问看声明类型,方法调用看实际类型
- 哪些没有多态:private、static、final 方法、构造器
掌握了底层机制,下一篇我们来看多态在实际开发中怎么用——参数多态化、数组多态、多态与工厂模式。你会发现,真正的面向对象设计,就是大量利用多态来完成的。
学完下一篇,你就能明白为什么有人说:好的代码不是写出来的,是「多态」出来的。