继承与多态:代码复用与千变万化
这是什么:继承(Inheritance) = 子类直接拿到父类的代码;多态(Polymorphism) = 同一句调用跑出不同的行为。
为什么用:上一章封装把数据锁好了,可猫、狗、鸟都有名字都会叫、叫声却不同——不搞面向对象,就得给每种动物各写一遍「名字、叫声」,重复到怀疑人生。继承和多态正是冲着这份重复来的。
如何用:本章先用 extends 写继承,再试一次 @Override 重写,最后体会一句调用跑出多种面孔的多态。
继承:extends
class Cat extends Animal 意思是:猫是一种动物,自动获得父类的成员变量和方法。构造方法里用 super(name) 调用父类构造方法,先把继承来的部分初始化好。
判断"该不该继承"有个黄金标准——is-a 测试:把关系读成一句话,"猫是一种动物",通顺,可以继承;"汽车是一种发动机",不通顺,那就不是继承关系,该用"汽车有一个发动机"的组合思路。新手最常见的滥用就是拿 extends 表达"部分与整体",读一句 is-a 就能当场识破。
继承还有两条硬规则:
| 规则 | 说明 |
|---|---|
Java 只能 extends 一个类 |
一个儿子只有一个亲爹,多重继承靠接口补(下一章) |
| 所有类都是 Object 的子孙 | 你不写 extends,默认继承 Object——toString、equals 就是从这来的 |
构造方法的执行顺序也值得一记:先父后子。创建 new Cat("咪咪") 时,super(name) 先把 Animal 那部分(name 字段)安顿好,再回来执行 Cat 自己的构造逻辑——就像装修房子先打地基再砌墙,顺序反不了。
方法重写:@Override
子类对父类的方法不满意,可以重写(override)同名方法,换成自己的实现。@Override 是个检查注解:如果你不小心把方法名拼错,编译器立刻报错,帮你当场抓 bug。
重写的合格标准:方法名相同、参数列表相同、返回值类型相同(或其子类),三同缺一不可。访问权限还有一条"就高不就低"的规矩:父类方法没加 private 或 static 这类限制的,才能被子类重写——构造方法和 private 方法不在重写赛道上。
初学者最绕的重写 vs 重载,一张表拆干净:
| 对比项 | 重写 override | 重载 overload |
|---|---|---|
| 位置 | 父子类之间 | 同一个类里(也可以跨继承) |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须完全相同 | 必须不同 |
| 返回值 | 相同(或子类型) | 任意(但它自己不算重载依据) |
| 一句话 | 子类"改台词" | 同名方法"多条台词" |
@Override 不写也能重写成功,但强烈建议永远写上——它让编译器替你把关"这真的是在重写吗"。设想你把 makeSound 手滑敲成 makeSond:不写注解,Java 以为你在定义一个全新方法,程序照跑,猫突然不叫了,你还得排查半天;写了注解,编译当场报错,一秒破案。
多态:一句调用,多种面孔
父类引用指向子类对象:Animal a = new Cat("咪咪");。调用 a.makeSound() 时,Java 看对象实际是谁——是猫就喵,是狗就汪。代码只写一遍,行为千变万化,这就是多态。
记这句口诀:编译看左边,运行看右边。Animal a = new Cat(...) 里,左边声明类型 Animal 决定"这个引用能调哪些方法"(编译器按它做安检),右边实际类型 Cat 决定"这些方法怎么跑"(运行时执行猫的版本)。
由此推出一个重要限制:父类引用调不到子类特有的方法。Cat 有个 scratch()(挠沙发),Animal a 这个引用看不见它——安检表上只登记了 Animal 的成员。想用就得"还原身份":
Animal a = new Cat("咪咪");
if (a instanceof Cat) { // 先问:你真是猫吗?
Cat c = (Cat) a; // 强转还原
c.scratch(); // 现在能调猫的特技了
}
instanceof 是"验明正身"的运算符,返回 boolean。先验后转是铁纪律——不验直接转,转错了运行时会抛异常。多态的真实工作流往往是:手里攥着一堆 Animal,先 instanceof 甄别,再各施各法。
完整示例
保存为 Main.java 运行(文件名跟 public 类名走):
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void makeSound() {
System.out.println(name + " 在叫");
}
@Override
public String toString() { // 所有类默认继承 Object,重写它打印更好看
return "Animal(" + name + ")";
}
}
class Cat extends Animal {
Cat(String name) {
super(name); // 调用父类构造方法
}
@Override
void makeSound() {
System.out.println(name + ":喵喵喵");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + ":汪汪汪");
}
}
public class Main {
public static void main(String[] args) {
Animal[] zoo = { new Cat("咪咪"), new Dog("旺财") };
for (Animal a : zoo) {
a.makeSound(); // 同一句调用,行为不同:多态
System.out.println(a); // 打印对象,自动调用重写后的 toString
}
}
}
运行后输出:
咪咪:喵喵喵
Animal(咪咪)
旺财:汪汪汪
Animal(旺财)
这段代码藏着一个精妙的点:Animal[] zoo 数组里能同时装猫和狗——因为"猫是一种动物"成立,子类对象可以放进父类数组。然后增强 for 挨个喊 a.makeSound(),喊到猫出猫声、喊到狗出狗声。设想动物园加一只鸟:只需要新增一个 Bird 类重写 makeSound,主循环一行不改——这就是多态对"扩展开放"的承诺:加新东西不加新 if。
再体会一下 toString 的地位:System.out.println(a) 打印的明明是个对象,出来的却是友好文本,因为 println 内部会自动调用对象的 toString()。它是最常被重写的方法,重写它等于给每个类配一张"名片"。
逐行解读
| 代码 | 作用 |
|---|---|
class Cat extends Animal |
继承:猫自动拥有 name、makeSound,只写自己的差异 |
super(name) |
调用父类构造方法,先初始化继承来的部分 |
@Override |
重写标记:拼错方法名编译器会立刻指出 |
Animal a = new Cat("咪咪") |
多态:父类引用指向子类对象 |
a.makeSound() |
实际类型说了算:是猫执行猫版,是狗执行狗版 |
System.out.println(a) |
println 自动调用 toString,重写后输出友好文本 |
补充一个 super 的隐藏用法:super 不只能在构造方法里用。在子类方法里写 super.makeSound(),可以调用父类的原版实现——典型场景是"先照父类的做,再补点自己的":
@Override
void makeSound() {
super.makeSound(); // 先执行父类版:"咪咪 在叫"
System.out.println("(撒娇中)喵喵喵"); // 再加点自己的戏
}
这种"扩展式重写"比"推倒重来"更省代码,父类逻辑有更新时子类自动跟着受益。
❓ 常见问题
Q1:Java 为什么不让一个类继承多个父类?
想象两个父类都有 makeSound,子类到底听谁的?这叫"菱形冲突"。Java 干脆规定单继承,把"多份标准"的需求交给接口(下一章)——接口只定标准不含实现,冲突小得多。
Q2:父类的 private 成员,子类继承得到吗?
继承得到但看不见:private 字段确实跟着对象一起存在,但子类代码不能直接访问,只能通过父类的 public 方法间接操作。这也呼应封装章的思想——私密的部分锁在父类自己家里。
Q3:Cat c = new Cat(...) 也能调 makeSound,多态多此一举吗?
单个对象看不出差别,差别在"批处理"。把猫、狗、鸟全塞进一个 Animal[],用一个循环统一调度——用一种类型管理千种子类,才是多态的主战场。真实项目里,"订单、退款、物流都实现了消息接口,一个循环统一推送"就是这么玩的。
Q4:构造方法能被继承或重写吗?
都不能。构造方法名字必须和类名一致,子类和父类名字都不同,天生无重写一说;子类要初始化父类部分,只能显式 super(...) 或依赖默认规则。
⚠️ 新手翻车现场
翻车一:不写 @Override 还拼错了方法名
class Cat extends Animal {
void makeSond() { // 手滑拼错,又没写注解
System.out.println(name + ":喵喵喵");
}
}
运行后输出:
咪咪 在叫
问题:猫不叫了!makeSond 是个全新的方法,父类的 makeSound 原封未动,多态悄悄失效——程序不报错,行为却错得离谱,这类"安静的 bug"最难排查。
修正:给重写方法统一加 @Override,拼错编译器当场报警。
翻车二:父类引用调子类特有方法
Animal a = new Cat("咪咪");
a.scratch(); // ❌ 编译报错
完整报错:
错误: 找不到符号
符号: 方法 scratch()
位置: 类型为Animal的变量 a
问题:编译器按"左边的声明类型 Animal"做安检,安检表上没有 scratch 这个方法,直接拦下。
修正:instanceof 验明正身后强转成 Cat 再调用。
翻车三:父类只有带参构造方法,子类忘了 super
class Animal {
Animal(String name) { this.name = name; }
}
class Cat extends Animal {
Cat() { // ❌ 编译报错
System.out.println("猫来了");
}
}
完整报错:
错误: 无法将类 Animal中的构造器 Animal应用到给定类型
问题:子类构造方法默认会先调父类的无参构造 super(),但父类只定义了带参版本,无参的"失踪"了,子类找不到人接班。
修正:子类构造里显式写 super("咪咪");,或者给父类补一个无参构造方法。
📝 本章小结
- 继承用 extends,先过 is-a 测试;Java 单继承,万类之祖是 Object
- 构造顺序先父后子,
super(...)负责调用父类构造方法 - 重写三同:方法名、参数、返回值都要同;
@Override永远写上,拼错当场报 - 重写是父子类改实现,重载是同类内换参数,别混
- 多态口诀:编译看左边(能调什么),运行看右边(怎么执行);父类引用调不到子类特有方法,要用先 instanceof 再强转
- 记忆口诀:extends 继家产,super 问爹要,@Override 显身手,父引用指向子对象,跑起来是多态。
记忆锚点:继承是"子承父业"——家产(字段)和手艺(方法)自动到手,手艺不满意的可以重修(重写);多态是"同一个岗位,各人各干法"——老板(循环)只喊开工,不关心你是猫工还是狗工;上岗前看合同(编译看左边),干活时看本人(运行看右边)。
动手改
- 加一个
Bird子类,makeSound 打印"叽叽叽",放进 zoo 数组再跑一遍 - 在
Cat里重写toString返回"猫咪(" + name + ")",看打印结果怎么变 - 试试让父类引用
a调用子类特有的方法,感受"父类引用看不到子类特有方法"的限制 - 给
Dog的 makeSound 加一句super.makeSound(),观察"扩展式重写"的输出顺序 - 写一个循环统计 zoo 里猫的数量(提示:
a instanceof Cat计数)
下一章:接口与常用集合,认识 Java 的内置工具箱 🐸