首页 / Java 入门教程 / 多态

Java 入门教程

多态

本教程共 100 篇 · 第 43 篇 · 更新于 2026-08-05 · 约 17 分钟阅读

JavaJava 入门教程多态向上转型instanceof动态绑定

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 方法都没有多态。

下一章讲**抽象类**——当父类的方法根本写不出实现时该怎么办。