4.4 多态
多态 = 同一条指令,交给不同的对象,跑出不同的结果。变量的类型是父类(钥匙上刻着"动物"),实际装的对象是子类(屋里可能是狗也可能是猫)——调用方法时,编译看左边(能不能调),运行看右边(调谁的)。
1. 从 4.3 的那行伏笔说起
4.3 结尾留了一行"混脸熟"的代码:
Animal a = new Dog();
当时说的是"父类钥匙指子类车"。现在把这行展开讲透——这就是多态的全部秘密:
- 变量
a的声明类型是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(); // 现在能调了
}
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(); // 同一行代码,狗叫狗的、猫叫猫的、鸟叫鸟的
}
好处总结成三句话:
- 统一管理:父类当参数、当数组元素,一网打尽所有子类
- 扩展友好:明年新增
Pig类,treat(Animal a)一行不用改,直接能用 - 分工明确:调用方只管"喊一声",具体怎么响应是每个对象自己的事
这背后就是 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 秒实验
- 属性无多态:给 Animal 加
int legs = 4;、Dog 里重写一份int legs = 4;(值不同,比如 3),打印((Animal)d).legs和d.legs——看"属性两边都看左边"是不是真的。 - 左边没有就调不了:
a.watchDoor();(不转型直接调 Dog 独有方法)——看编译器怎么拦。 - 不验货硬转:把 Cat 对象强转成 Dog 跑一次——亲眼见
ClassCastException长什么样。以后见到这个报错,三秒定位"哪儿没 instanceof"。
2.7 ArrayIndexOutOfBoundsException(数组越界)、3.2 NullPointerException(空指针)、今天 ClassCastException(类型转换)——全部见过实物,排错时就不再怕了。
8. 常见误区
重写是前提,多态是效果。有重写、没有"父类引用指子类对象",就没有多态——只是普通的换版本。
对象自始至终都是 Dog,变的只是你"看它的角度"。instanceof Dog 返回 true 就是证明——盒子里的狗不会因为钥匙刻了"动物"就变成动物本类。
恰恰相反,属性编译运行都看左边。这是方法与属性最大的不同,选择题最爱。
编译器不拦(它只查左边类型是否父子相关),运行时才炸。省一行 instanceof,换来一个线上事故——不划算。
接口同样玩多态,而且用得更多:List list = new ArrayList();、4.3 的 Flyable f = new Bird();——左合同右实现,是多态最主流的用法。
9. 自测题(先自己答,再展开)
① 有继承(或接口实现);② 有方法重写;③ 父类引用指向子类对象(Animal a = new Dog();)。缺①装不进去(编译报错);缺②跑的全是父类版本,没有"多种形态";缺③只是普通调用,行为固定。
a.sound():编译时查 Animal 有没有 sound()——有,放行;运行时按实际对象 Cat 执行——输出喵喵。a.catchMouse():编译直接报错——catchMouse 只有 Cat 有,Animal 清单里没有,编译器只认左边。
用 Animal 的。成员变量没有多态——编译看左边,运行也看左边。和方法的"运行看右边"区别开,这是本节第一大特例。
多态让一个方法收下所有子类对象(统一管理);将来新增 Pig 类,treat 一行不用改(扩展友好);调用方只管喊 sound(),具体行为由对象自己决定(分工明确)。这就是"对扩展开放、对修改关闭"思想的雏形。
Animal b = new Cat();
if (b instanceof Cat) {
Cat c = (Cat) b;
c.catchMouse();
}
不写 instanceof:如果 b 里装的其实是 Dog(或其他非 Cat 对象),编译照常通过,运行时抛 ClassCastException——编译器只查"类型上能不能转"(Cat 和 Dog 同属 Animal 家族,语法上放行),实际对象对不对只有运行时知道。所以向下转型必须先 instanceof 验货。
10. 下一步学什么
- ☑ 4.5 Object类 ✅ —— 4.1"万物之祖"伏笔收网:toString 解谜"类名@乱码"、equals vs ==(String 常量池大坑)、hashCode 搭伴规矩(集合章收网)。点此复习
相关笔记
- 4.3 抽象类和接口 ← 上一篇:抽象类/接口立规矩 +
Animal a = new Dog()伏笔转正 - 4.1 继承 ← 多态的前提①:extends 白得成员
- 3.2 类与对象 ← 栈钥匙、堆实物:多态的地基
- 2.3 Java中的运算符 ← instanceof 出来的 boolean 就住在这
- 📇 Java基础 · 索引
- 🏠 返回主页