多态
本教程共 100 篇 · 第 43 篇 · 更新于 2026-08-05 · 约 17 分钟阅读
43. 多态
本节目标:理解「变量类型」和「对象类型」的分离,掌握向上/向下转型和
instanceof,并明白多态为什么能让代码「新增功能不改旧代码」。
一个反直觉的运行结果
先看代码,猜结果:
class Person {
public void run() { System.out.println("Person.run"); }
}
class Student extends Person {
@Override
public void run() { System.out.println("Student.run"); }
}
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run(); // 打印什么?
}
}
```bash
变量 `p` 声明成 `Person`,很多人会猜 `Person.run`。
实际输出是 `Student.run`。
这就是**多态**:Java 调用实例方法时,看的是**对象运行时的真实类型**,而不是变量声明的类型。这个机制叫**动态绑定**。
## 变量类型 vs 对象类型
理解多态的关键,是把两个概念彻底分开。
```java
Person p = new Student();
// ↑ ↑
// 声明类型 实际类型
- 声明类型(
Person)决定:编译器允许你调用哪些方法。 - 实际类型(
Student)决定:运行时真正执行哪份实现。
Person p = new Student();
p.run(); // 编译器查 Person 有没有 run() → 有,放行
// 运行时看对象是 Student → 执行 Student.run()
// p.study(); // 编译错误!Person 没有 study() 方法
// 哪怕对象确实是 Student 也不行
```text
把那行注释去掉再编译,报错长这样:
```text
Main.java:8: error: cannot find symbol
p.study();
^
symbol: method study()
location: variable p of type Person
注意最后一行 variable p of type Person——编译器只认变量 p 的声明类型,压根不管它运行时装的是谁。
一句话总结:编译看左边,运行看右边。
向上转型:子类当父类用
把子类对象赋值给父类变量,叫向上转型(upcasting)。
Student s = new Student();
Person p = s; // 向上转型,自动完成,永远安全
Object o = s; // 转到更上层,同样安全
```bash
为什么安全?因为 `Student` 拥有 `Person` 的全部功能。用 `Person` 的视角去看一个 `Student`,能看到的东西它一定都有。
向上转型不需要写强制类型转换,编译器自动完成。
### 向上转型的实际价值
它让方法能接收「一整族」类型:
```java
public static void makeItRun(Person p) {
p.run();
}
// 全都能传进去
makeItRun(new Person());
makeItRun(new Student());
makeItRun(new Teacher());
写一个方法,服务所有子类。这就是多态最直接的收益。
向下转型:父类变回子类
反过来,把父类变量强制转成子类类型,叫向下转型(downcasting)。它必须显式写,而且可能失败。
Person p1 = new Student(); // 实际对象是 Student
Person p2 = new Person(); // 实际对象是 Person
Student s1 = (Student) p1; // OK,对象本来就是 Student
Student s2 = (Student) p2; // 运行时抛 ClassCastException!
```java
第二行为什么崩?因为 `p2` 指向的是一个纯 `Person` 对象,它没有 `Student` 的那些额外功能。子类比父类多的东西,变不出来。
崩的时候控制台是这副样子:
```text
Exception in thread "main" java.lang.ClassCastException:
class Person cannot be cast to class Student
at Main.main(Main.java:12)
看到 cannot be cast to 就能定位问题:转型的目标类型和对象的真实类型对不上。
Warning
ClassCastException是运行时异常,编译期发现不了。所以向下转型前一定要先判断类型。
instanceof:转型前先体检
instanceof 判断一个对象是不是某个类型(或其子类)的实例:
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Student s = new Student();
System.out.println(s instanceof Person); // true —— 子类也算父类
System.out.println(s instanceof Student); // true
Person n = null;
System.out.println(n instanceof Person); // false —— null 对任何类型都是 false
```java
最后一条很实用:`instanceof` 自带 null 检查,不会因为空引用抛异常。
有了它,向下转型就安全了:
```java
Person p = getPerson(); // 不确定实际类型
if (p instanceof Student) {
Student s = (Student) p; // 判断通过,转型一定成功
System.out.println(s.getScore());
}
模式匹配:Java 16 起的简洁写法
上面那段代码有点啰嗦:判断了一次类型,又强制转换了一次,Student 这个词写了三遍。
从 Java 16 开始(JEP 394,Java 14 起预览,Java 16 转正),instanceof 支持模式匹配,判断和转型一步到位:
Person p = getPerson();
if (p instanceof Student s) { // 判断成功就自动把结果绑定到变量 s
System.out.println(s.getScore());
}
```bash
`s` 叫「模式变量」,只在判断为 true 的分支里有效。
新旧写法对照:
```java
// 历史兼容:Java 15 及以前
Object obj = "hello";
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str.toUpperCase());
}
// 主线写法:Java 16+
Object obj = "hello";
if (obj instanceof String str) {
System.out.println(str.toUpperCase());
}
模式变量还能和逻辑运算符配合:
if (obj instanceof String str && str.length() > 3) {
System.out.println(str); // && 右边可以直接用 str
}
```bash
> [!TIP]
> Java 25 是本教程的基线版本,模式匹配早已转正,日常写代码请直接用新写法。旧写法只在维护老项目时才会遇到。
### 转型速查
| 方向 | 写法 | 要不要强转 | 风险 |
|------|------|------------|------|
| 向上转型(子 → 父) | `Person p = new Student();` | 不要 | 无,永远安全 |
| 向下转型(父 → 子) | `Student s = (Student) p;` | 要 | 类型不符抛 `ClassCastException` |
| 模式匹配(父 → 子) | `if (p instanceof Student s)` | 不要 | 无,判断失败就进不去分支 |
三行里只有中间那行有翻车风险,能用模式匹配就别裸着强转。
## 多态的真正威力:开闭原则
前面的例子都太小,看不出多态到底好在哪。来个像样的。
假设要做个报税功能。收入有多种,计税规则各不相同。
先定义一个基类:
```java
class Income {
protected double amount;
public Income(double amount) { this.amount = amount; }
public double getTax() {
return amount * 0.1; // 默认税率 10%
}
}
工资收入有起征点:
class Salary extends Income {
public Salary(double amount) { super(amount); }
@Override
public double getTax() {
if (amount <= 5000) return 0;
return (amount - 5000) * 0.2;
}
}
```java
政府特殊津贴全额免税:
```java
class SpecialAllowance extends Income {
public SpecialAllowance(double amount) { super(amount); }
@Override
public double getTax() { return 0; }
}
现在写汇总方法。注意它只认识 Income:
public static double totalTax(Income... incomes) {
double total = 0;
for (Income income : incomes) {
total += income.getTax(); // 每个对象执行自己那份 getTax()
}
return total;
}
```java
这个方法完全不知道 `Salary`、`SpecialAllowance` 的存在,却能正确算出每一种的税。
**关键在于**:哪天要新增「稿费收入」,只需要写一个新类:
```java
class Royalty extends Income {
public Royalty(double amount) { super(amount); }
@Override
public double getTax() {
return amount * 0.14;
}
}
totalTax() 一行都不用改,直接就支持了。
这就是设计原则里的开闭原则:对扩展开放,对修改封闭。没有多态,你就得在 totalTax() 里写一长串 if-else 判断类型,每加一种收入就改一次——改着改着就改出 bug 了。
完整可运行示例
public class Main {
public static void main(String[] args) {
Income[] incomes = {
new Income(3000),
new Salary(7500),
new SpecialAllowance(15000),
new Royalty(10000)
};
System.out.println("总税额: " + totalTax(incomes));
// 逐项打印,顺便演示模式匹配
for (Income income : incomes) {
String type = describe(income);
System.out.printf("%s: 收入 %.0f, 税 %.2f%n",
type, income.amount, income.getTax());
}
}
public static double totalTax(Income... incomes) {
double total = 0;
for (Income income : incomes) {
total += income.getTax();
}
return total;
}
// Java 16+ 模式匹配
public static String describe(Income income) {
if (income instanceof Salary s) {
return "工资(起征点5000)";
} else if (income instanceof SpecialAllowance a) {
return "特殊津贴(免税)";
} else if (income instanceof Royalty r) {
return "稿费(14%)";
}
return "普通收入(10%)";
}
}
class Income {
protected double amount;
public Income(double amount) { this.amount = amount; }
public double getTax() { return amount * 0.1; }
}
class Salary extends Income {
public Salary(double amount) { super(amount); }
@Override
public double getTax() {
if (amount <= 5000) return 0;
return (amount - 5000) * 0.2;
}
}
class SpecialAllowance extends Income {
public SpecialAllowance(double amount) { super(amount); }
@Override
public double getTax() { return 0; }
}
class Royalty extends Income {
public Royalty(double amount) { super(amount); }
@Override
public double getTax() { return amount * 0.14; }
}
```bash
```bash
javac Main.java
java Main
输出:
总税额: 1200.0
普通收入(10%): 收入 3000, 税 300.00
工资(起征点5000): 收入 7500, 税 500.00
特殊津贴(免税): 收入 15000, 税 0.00
稿费(14%): 收入 10000, 税 1400.00
```bash
## 三个容易混的边界情况
**字段没有多态。** 字段访问看的是声明类型,不走动态绑定:
```java
class A { String name = "A"; }
class B extends A { String name = "B"; }
A a = new B();
System.out.println(a.name); // "A" —— 不是 "B"!
这又一次说明:不要定义同名字段。
静态方法没有多态。 static 方法属于类,同样看声明类型:
class A { static void hi() { System.out.println("A.hi"); } }
class B extends A { static void hi() { System.out.println("B.hi"); } }
A a = new B();
a.hi(); // "A.hi" —— 而且 IDE 会提示你应该写 A.hi()
```bash
调静态方法请老老实实用类名,别用对象引用。
**private 方法没有多态。** 子类看不见父类的 `private` 方法,也就无从重写。
一句话收口:**只有非 static、非 private、非 final 的实例方法才有多态。**
## 常见疑问
**Q:多态会不会拖慢程序?**
动态绑定确实要多查一次方法表,但 JVM 的即时编译器会做内联优化,绝大多数场景下这点开销可以忽略。为了这点性能放弃多态,得不偿失。
**Q:向上转型之后,子类特有的方法就永远调不到了吗?**
调不到,除非再向下转型回去。如果你发现代码里到处都在向下转型,通常说明抽象层没设计好——要么该把那个方法提到父类里,要么该换个抽象层次。
**Q:`instanceof` 和 `getClass() == X.class` 有什么区别?**
`instanceof` 认子类,`getClass()` 比较只认同一个类。判断「是不是这一族」用 `instanceof`;写 `equals()` 时要求严格同类,才用 `getClass()`。
## 小结
- 多态 = 调用实例方法时按对象的实际类型执行,编译看左边、运行看右边。
- 向上转型(子 → 父)自动且安全,是多态的前提。
- 向下转型(父 → 子)需要强制转换,可能抛 `ClassCastException`。
- 转型前用 `instanceof` 判断;Java 16+ 直接用 `if (o instanceof Type t)` 模式匹配。
- 多态让「新增子类不改旧代码」成为可能,这是开闭原则的落地方式。
- 字段、静态方法、private 方法都没有多态。
下一章讲**抽象类**——当父类的方法根本写不出实现时该怎么办。