☕ 第四章 面向对象(下)

4.4 多态

📄 一句话概括

多态 = 同一条指令,交给不同的对象,跑出不同的结果。变量的类型是父类(钥匙上刻着"动物"),实际装的对象是子类(屋里可能是狗也可能是猫)——调用方法时,编译看左边(能不能调),运行看右边(调谁的)。

1. 从 4.3 的那行伏笔说起

4.3 结尾留了一行"混脸熟"的代码:

Animal a = new Dog();

当时说的是"父类钥匙指子类车"。现在把这行展开讲透——这就是多态的全部秘密:

同一个变量类型,可以装任何子类对象:

Animal a = new Dog();   // a 这把"动物钥匙"开的是狗
Animal b = new Cat();   // 同款钥匙,开的是猫

而调用方法时——

a.sound();   // 输出:汪汪汪   (调的是 Dog 重写的版本)
b.sound();   // 输出:喵喵喵   (调的是 Cat 重写的版本)

代码一行没变,行为随对象而变——这就是"多态"(多种形态)这个名字的由来。

2. 多态的三个前提(缺一不可)

前提 对应学过的知识 缺了会怎样
① 有继承(或实现) 4.1 extends / 4.3 implements 没有父子关系,装不进去
② 有方法重写 子类对白得的方法"不满意",自己换版本 全跑父类的版本,没有"多种形态"
③ 父类引用指向子类对象 Animal a = new Dog(); 没有这个"张冠李戴",就只是普通调用

考试和面试问"多态的条件",答这三条,一字不差。

3. 核心口诀:编译看左边,运行看右边

这是本节最重要的一句话,拆开看:

编译阶段(javac,1.4 学过):
  只看 = 左边的类型 Animal
  任务 = 检查"Animal 有没有 sound() 这个方法"
  → 有,放行;没有,编译报错(哪怕对象里明明有也不行)

运行阶段(JVM):
  只看 = 右边的实际对象 Dog / Cat
  任务 = 真正执行"这个对象自己的 sound() 版本"

为什么要这么设计? 编译器必须保证安全——它只知道你声明的是 Animal,就只能按 Animal 的"功能清单"检查;JVM 运行时打开盒子看见里面真是一只狗,就让狗自己表演。编译管安全,运行管真实。

⚠️ 最容易懵的推论:左边没有的方法,不能调
Animal a = new Dog();
a.watchDoor();   // 编译报错!

watchDoor() 只有 Dog 有、Animal 没有——编译器只查左边的清单,直接拦下。对象里明明有这功能,却调不了,这就是多态的"代价",解法见第 5 节的向下转型。

4. 特例:成员变量没有多态!

方法有多态,属性没有。规则一句话:

成员变量:编译看左边,运行也看左边(只认父类的)
成 员 方 法:编译看左边,运行看右边(认实际对象的)
class Animal { int legs = 4;  void sound() {...} }
class Dog extends Animal { int legs = 4; String name = "狗"; void sound() {...} }

Animal a = new Dog();
a.sound();    // Dog 的版本(方法:运行看右边)
System.out.println(a.legs);   // 4——运行也看左边,取 Animal 的 legs
// a.name      // 编译报错:Animal 里没有 name

为什么? 属性没有"重写"这回事(那叫隐藏),JVM 也没必要动态分派——记住"属性两边都看左边"这个特例就行,它是选择题常客。

5. 转型:向上自然,向下要验

向上转型(自动,天天在用)

Animal a = new Dog();        // 子类 → 父类,自动

代价:a 只能调 Animal 清单里的方法,Dog 独有的功能暂时"看不见"。

向下转型(强制,要先验货)

把"动物钥匙"换回"狗钥匙",才能调 Dog 独有的方法:

Animal a = new Dog();
if (a instanceof Dog) {          // 先问:里面真的是狗吗?
    Dog d = (Dog) a;             // 强制转换,格式:(子类名) 变量
    d.watchDoor();               // 现在能调了
}
🚨 不验货硬转 = ClassCastException(类型转换异常)
Animal b = new Cat();
Dog d = (Dog) b;   // 编译能过,运行炸:猫怎么能转成狗?!

和 2.7 的越界异常、3.2 的空指针并列——Java 三大新手异常齐了。转之前先 instanceof 问一句,是职业习惯。

instanceof 的读法:"……是不是……的实例"。a instanceof Dog → a 指向的对象是 Dog 或它的子类吗?返回 boolean(2.3 老朋友上岗)。

6. 多态到底好在哪?(这是本节的灵魂)

多态不是炫技,它解决一个实际问题:怎么让代码对"将来才出现的东西"友好?

看一个"宠物医院"例子:

// 不用多态:每来一种新动物,就要加一个重载方法
void treat(Dog d) { d.sound(); }
void treat(Cat c) { c.sound(); }
void treat(Bird b) { b.sound(); }   // 又来了鸟,又要改这里……

// 用多态:一个方法管所有动物,管到天荒地老
void treat(Animal a) { a.sound(); }   // 参数是父类 = 什么动物都收

调用侧的效果:

Animal[] pets = { new Dog(), new Cat(), new Bird() };
for (Animal p : pets) {
    p.sound();   // 同一行代码,狗叫狗的、猫叫猫的、鸟叫鸟的
}

好处总结成三句话:

  1. 统一管理:父类当参数、当数组元素,一网打尽所有子类
  2. 扩展友好:明年新增 Pig 类,treat(Animal a) 一行不用改,直接能用
  3. 分工明确:调用方只管"喊一声",具体怎么响应是每个对象自己的事

这背后就是 4.3 说的设计思想:抽象类/接口立规矩,多态让规矩"活"起来——三者合体,才是面向对象真正的威力。

💡 你早就用过多态了

4.3 动手时写的 Animal a = new Bird(); a.fly();,还有 IDEA 里所有 List<String> list = new ArrayList<>();——左边接口、右边实现类,这就是接口多态,Java 圈最常用的姿势。将来学集合、学框架,天天都是它。

7. 动手:让一排动物挨个表演

在 IDEA04 项目(4.1 建的)里新建 TestPoly:

// 前提:4.1/4.3 的 Animal(抽象类)、Dog、Cat、Bird 都在
public class TestPoly {
    public static void main(String[] args) {
        // ① 父类数组装各种子类对象(多态入场)
        Animal[] zoo = { new Dog(), new Cat(), new Bird() };

        // ② 同一行代码,三种输出
        for (Animal a : zoo) {
            a.sound();          // 编译看左,运行看右
        }

        // ③ instanceof 验货 + 向下转型
        for (Animal a : zoo) {
            if (a instanceof Dog) {
                Dog d = (Dog) a;
                System.out.println("这只狗会看门");
            }
        }
    }
}

成功标准:输出里 sound() 各按各的叫(狗汪、猫喵、鸟啾),而"这只狗会看门"只打印一次——同一行代码 a.sound() 出现三种结果,就是多态的现场证据。

三个 30 秒实验

  1. 属性无多态:给 Animal 加 int legs = 4;、Dog 里重写一份 int legs = 4;(值不同,比如 3),打印 ((Animal)d).legs 和 d.legs——看"属性两边都看左边"是不是真的。
  2. 左边没有就调不了:a.watchDoor();(不转型直接调 Dog 独有方法)——看编译器怎么拦。
  3. 不验货硬转:把 Cat 对象强转成 Dog 跑一次——亲眼见 ClassCastException 长什么样。以后见到这个报错,三秒定位"哪儿没 instanceof"。
📝 三个新手大异常集合完毕

2.7 ArrayIndexOutOfBoundsException(数组越界)、3.2 NullPointerException(空指针)、今天 ClassCastException(类型转换)——全部见过实物,排错时就不再怕了。

8. 常见误区

⚠️ 误区 1:多态就是方法重写

重写是前提,多态是效果。有重写、没有"父类引用指子类对象",就没有多态——只是普通的换版本。

⚠️ 误区 2:`Animal a = new Dog();` 后 a 就变成了 Animal 对象

对象自始至终都是 Dog,变的只是你"看它的角度"。instanceof Dog 返回 true 就是证明——盒子里的狗不会因为钥匙刻了"动物"就变成动物本类。

⚠️ 误区 3:属性也遵循"运行看右边"

恰恰相反,属性编译运行都看左边。这是方法与属性最大的不同,选择题最爱。

⚠️ 误区 4:向下转型直接 `(Dog) a` 就行,instanceof 多此一举

编译器不拦(它只查左边类型是否父子相关),运行时才炸。省一行 instanceof,换来一个线上事故——不划算。

⚠️ 误区 5:多态只存在于类之间

接口同样玩多态,而且用得更多:List list = new ArrayList();、4.3 的 Flyable f = new Bird();——左合同右实现,是多态最主流的用法。

9. 自测题(先自己答,再展开)

❓ Q1:多态的三个前提是什么?缺一个会怎样?

① 有继承(或接口实现);② 有方法重写;③ 父类引用指向子类对象(Animal a = new Dog();)。缺①装不进去(编译报错);缺②跑的全是父类版本,没有"多种形态";缺③只是普通调用,行为固定。

❓ Q2:`Animal a = new Cat(); a.sound(); a.catchMouse();` 各发生什么?

a.sound():编译时查 Animal 有没有 sound()——有,放行;运行时按实际对象 Cat 执行——输出喵喵。a.catchMouse():编译直接报错——catchMouse 只有 Cat 有,Animal 清单里没有,编译器只认左边。

❓ Q3:`Animal a = new Dog();` 后 `a.legs` 用谁的?(Animal 和 Dog 都定义了 legs,值不同)

用 Animal 的。成员变量没有多态——编译看左边,运行也看左边。和方法的"运行看右边"区别开,这是本节第一大特例。

❓ Q4:为什么 `void treat(Animal a)` 比 `void treat(Dog d)`、`void treat(Cat c)` 写一堆重载好?

多态让一个方法收下所有子类对象(统一管理);将来新增 Pig 类,treat 一行不用改(扩展友好);调用方只管喊 sound(),具体行为由对象自己决定(分工明确)。这就是"对扩展开放、对修改关闭"思想的雏形。

❓ Q5(毕业题):写一段代码,把 `Animal b = new Cat();` 安全地转回 Cat 并调用 Cat 独有方法,并说明不写 instanceof 的风险
Animal b = new Cat();
if (b instanceof Cat) {
    Cat c = (Cat) b;
    c.catchMouse();
}

不写 instanceof:如果 b 里装的其实是 Dog(或其他非 Cat 对象),编译照常通过,运行时抛 ClassCastException——编译器只查"类型上能不能转"(Cat 和 Dog 同属 Animal 家族,语法上放行),实际对象对不对只有运行时知道。所以向下转型必须先 instanceof 验货。

10. 下一步学什么


相关笔记