02继承和多态
- 认识继承
- 权限修饰符
- 继承的特点
- 方法重写
- 子类构造器的特点
Java OOP 核心笔记:继承与多态
1. 认识继承 (Inheritance)
1.1 什么是继承?
继承是 Java 面向对象编程(OOP)的三大特征之一(封装、继承、多态)。它允许我们创建一个新的类(子类/派生类),从已有的类(父类/超类/基类)那里直接获得属性和方法。
- 核心关键字:
extends - 格式:
- 本质逻辑:is-a 关系。例如:
Student is a Person(学生是一个人),Dog is an Animal(狗是一种动物)。
1.2 继承的好处
- 代码复用:父类写一次,所有子类都能用。
- 便于维护:修改父类逻辑,所有子类自动同步更新。
- 多态的前提:没有继承就没有多态。
1.3 内存中的继承(重要)
当创建一个子类对象时,内存中会发生什么?
- 在堆内存(Heap)中,子类对象其实包含了一个父类对象的“分身”。
- 即:子类对象 = 父类的部分 + 子类特有的部分。
2. 权限修饰符 (Access Modifiers)
在继承关系中,最重要的就是搞清楚:爸爸的东西,儿子到底能不能用?
Java 有四种权限修饰符,按访问能力从大到小排列如下:
| 修饰符 | 关键字 | 本类中 | 本包中 (子类/无关类) | 不同包的子类 | 不同包的无关类 | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 公共 | public | ✅ | ✅ | ✅ | ✅ | ||||||
| 受保护 | protected | ✅ | ✅ | ✅ (重点) | ❌ | ||||||
| 默认 | (default) | ✅ | ✅ | ❌ | ❌ | ||||||
| 私有 | private | ✅ | ❌ | ❌ | ❌ |
继承中的特殊说明:
-
private (私有) :- 能继承吗? 严格来说,能继承(子类对象内存里确实有这个字段)。
- 能访问吗? 不能直接访问。儿子拥有父亲的私房钱,但父亲锁在保险柜里,儿子打不开。必须通过父类提供的
public 的get/set方法来间接访问。
-
protected (受保护) :- 这是专门为继承设计的。它的含义是:“外人(不同包无关类)不能动,但我的子孙后代(哪怕在天涯海角的不同包里)可以动”。
3. 继承的特点
Java 的继承机制非常严格,有以下三个铁律:
3.1 单继承 (Single Inheritance)
- 规则:一个子类只能有一个直接父类。
- 代码体现:
- 原因:为了防止“钻石问题”(如果 B 和 C 都有
run()方法,A 继承谁的?Java 为了安全避开了这个问题)。
3.2 多层继承 (Transitivity)
-
规则:继承是可以传递的。
-
例子:C 继承 B,B 继承 A。那么 C 不仅有 B 的功能,也有 A 的功能。
爷爷 -> 爸爸 -> 孙子(孙子拥有爷爷的基因)。
3.3 所有类的祖宗:Object 类
- 规则:如果一个类没有显式地写
extends,那么它默认继承Object类。 -
Object 是 Java 类层级结构的根(Root)。这也就是为什么任何对象都能调用toString()、equals()方法的原因。
3.4就近原则:
-
继承后子类访问成员的特点:就近原则
优先访问自己类中,自己类中的没有才会访问父类
-
在子类方法中访问其他成员(成员变量、成员方法),是依照就近原则的。
先子类局部范围找,然后子类成员范围找,然后父类成员范围找,如果父类范围还没有找到则报错。 -
如果子父类中,出现了重名的成员,会优先使用子类的, 如果此时一定要在子类中使用父类的怎么办?
可以通过super关键字,指定访问父类的成员:
4. 方法重写 (Override)
当父类的方法无法满足子类的需求时,子类可以重写该方法。
4.1 什么是重写?
- 场景:父类有
call()方法(打电话),子类手机想在打电话前增加“显示头像”的功能。 - 实现:子类定义一个与父类方法签名完全一样的方法。
4.2 必须遵守的规则 (重点面试题)
我们可以总结为: “两同、两小、一大”
-
两同:
- 方法名必须相同。
- 参数列表必须相同。
-
一大 (权限) :
- 子类方法的访问权限 父类方法的访问权限。
- 例子:父类是
protected,子类可以是protected 或public,但不能是private(不能越活越回去)。(不能越权访问,等级森严这一块)
-
两小 (返回值与异常) :
- 返回值类型:子类方法的返回值类型,必须与父类相同,或者是父类返回类型的子类。
- 抛出的异常:子类抛出的异常不能比父类更多(只能是父类异常的子类或不抛出)。
4.3 核心注意事项
-
@Override 注解:强烈建议写上!它能帮编译器检查你是不是写错了(比如名字打错字母),起到安全校验的作用。 - 私有方法不能重写:父类的
private方法对子类不可见,子类就算写了一个一模一样的,那也不叫重写,那叫子类自己新定义了一个方法。
!!!实际开发过程当中,直接写一样的返回类型就好了!!!访问权限可以更大,但一般都是使用public和private
5. 子类构造器的特点 (Constructor)
这是继承中最容易出错的地方。构造器是不能被继承的(你不能说儿子直接继承了爸爸的名字),但子类初始化时必须调用父类的构造器。
5.1 核心原则
子类构造器执行时,必须先执行父类的构造器。
为什么?因为子类继承了父类的数据,在子类初始化之前,必须先让父类把数据初始化好(先有爸爸,才有儿子)。
5.2 代码执行流程
-
隐式调用 (
super() ) :
如果你在子类构造器里什么都不写,Java 编译器会默认在第一行加上super();,即调用父类的无参构造器。 -
显式调用 (
super(参数) ) :
如果父类没有无参构造器(只写了带参构造器),那么子类必须在构造器的第一行,手动写上super(参数)来调用父类的带参构造器,否则编译报错。 -
this(...) 和 super(...) 的冲突:-
super(...)必须在第一行。 -
this(...)(调用本类其他构造器) 也必须在第一行。 - 结论:在同一个构造器中,
super(...) 和this(...) 不能同时出现。
-
-
this(...) 调用兄弟构造器 的核心要点,可以把它看作是 “构造器内部的外包机制” 。1. 核心定义
- 含义:在同一个类中,一个构造器去调用另一个构造器。
- 目的:复用代码(DRY 原则)。防止在多个构造器里写重复的初始化代码(如
this.name = name写好几遍)。
2. 最佳实践模式:漏斗模式 (The Funnel)
“小构造器” 调用 “大构造器”。
- 大构造器(全参):负责真正干活,包含所有的初始化逻辑。
- 小构造器(无参/少参):只负责给默认值,然后把活儿外包给大构造器。
3. 三大铁律 (死规矩)
- 位置霸道:
this(...) 必须写在构造器的 第一行(和super一样霸道)。 - 一山不容二虎:同一个构造器里,不能同时出现
this(...) 和super(...)。(因为都争第一行)。 - 禁止套娃:严禁递归调用(A 调 B,B 又调 A),编译器会报错。
4. 一眼看懂的代码模板
// 1. 全参构造器(大哥):真正干活的,处于漏斗最底层 public Hero(String name, int hp) { this.name = name; this.hp = hp; } // 2. 无参构造器(小弟):只给默认值,转手包给大哥 public Hero() { // 翻译:我没参数,但我委托大哥创建一个叫"路人甲"、血量100的人 this("路人甲", 100); }
总结图谱
- 继承:
extends,单继承,查找关系是this ->super ->super... - 修饰符:
private (父类独有),protected (传家宝),public(大家用)。 - 重写:方法头完全一致,权限不能变小,逻辑变新。
- 构造器:先父后子,第一行必须是
super(显式或隐式)。
第二部分:多态 (Polymorphism) —— “花木兰替父从军”
多态是 Java 面向对象中最难理解,但最强大的部分。
1. 什么是多态?
字面意思:一种事物,多种形态。
代码意思:父类的引用,指向了子类的对象。
这是多态的终极公式:
2. 为什么需要多态?(生动案例)
假设您在写一个“战场模拟器”,需要让所有英雄一起攻击。
如果没有多态:
Java
// 你需要给每个职业写一个方法
public void warriorAttack(Warrior w) { w.attack(); }
public void mageAttack(Mage m) { m.attack(); }
public void archerAttack(Archer a) { a.attack(); }
// 如果以后出了个“刺客”,你还得回来改代码加方法,太累了!
有了多态:
您只需要看他们的共同身份——Hero。
Java
// 只需要写一个方法,接收所有 Hero 的子类!
public void makeHeroAttack(Hero h) {
h.attack(); // 这里的 h 具体是谁?运行的时候才知道!
}
3. 多态的三大前提
要想实现多态,缺一不可:
- 要有继承关系 (
Warrior extends Hero) - 要有方法重写 (子类重写了
attack方法) - 父类引用指向子类对象 (
Hero h = new Warrior())
4. 详细代码演示(从内存看本质)
Java
// 1. 父类
class Hero {
void attack() {
System.out.println("英雄用拳头攻击");
}
}
// 2. 子类 Warrior 重写方法
class Warrior extends Hero {
@Override
void attack() {
System.out.println("战士挥舞大剑劈砍!");
}
void defense() {
System.out.println("战士举盾防御");
}
}
// 3. 子类 Mage 重写方法
class Mage extends Hero {
@Override
void attack() {
System.out.println("法师释放大火球!");
}
}
public class GameStart {
public static void main(String[] args) {
// --- 向上转型 (Upcasting) ---
// 这里的 h 就像是“花木兰”
// 她的外表(编译类型)是 Hero(父亲)
// 她的灵魂(运行类型)是 Warrior(女儿)
Hero h = new Warrior();
// --- 核心看点 ---
// 编译看左边:编译器去 check Hero 类里有没有 attack 方法?有,编译通过。
// 运行看右边:真正运行的时候,执行的是 Warrior 的 attack 方法!
h.attack(); // 输出:战士挥舞大剑劈砍!
// h.defense(); // 报错!
// 为什么?因为花木兰现在穿着父亲的铠甲,父亲没有“举盾”这个技能,
// 只有卸下伪装(向下转型)才能用。
}
}
第三部分:深水区 —— 转型与 instanceof
既然父类引用 (Hero h) 看不到子类特有的方法 (defense),那如果我非要用子类特有的方法怎么办?
1. 向下转型 (Downcasting) —— “卸下伪装”
我们需要强制把 Hero 类型的引用变回 Warrior 类型。
Java
Hero h = new Warrior();
h.attack(); // 多态调用
// 强制类型转换
Warrior w = (Warrior) h;
w.defense(); // 成功!现在可以使用战士的特有技能了
2. 只有特定情况才能强转
如果你的对象本质是法师,你非要把它转成战士,就会出大问题:
Java
Hero h = new Mage(); // 本质是 Mage
Warrior w = (Warrior) h; // 编译不报错,但运行会报 ClassCastException!(类型转换异常)
// 就像你指着一只猫说:“变成狗!” 这是不可能的。
3. 安全锁:instanceof
在强转之前,务必判断一下“你到底是不是这个类型”:
-
2. 语法格式
Java
boolean result = 对象引用 instanceof 类名;- 左边:是一个引用变量(对象)。
- 右边:是一个类(或接口)。
- 结果:返回
true(是这个类型)或false(不是)。
Java
public void checkHero(Hero h) {
if (h instanceof Warrior) {
Warrior w = (Warrior) h;
w.defense(); // 安全转型
System.out.println("这是一个战士");
} else if (h instanceof Mage) {
Mage m = (Mage) h;
// m.blink(); // 假设法师有闪现
System.out.println("这是一个法师");
}
}
第四部分:综合实战案例 —— 简易游戏引擎
让我们用一个完整的案例把继承、重写、多态、向上转型整合起来。
需求:
- 有一个
GameEngine(游戏引擎)。 - 它可以接收任何类型的怪物
Monster。 - 不论传入的是
Slime(史莱姆)还是Dragon(巨龙),引擎都能让它们受到伤害。
Java
// 1. 基类:怪物
class Monster {
String name;
public Monster(String name) { this.name = name; }
public void getHit() {
System.out.println(name + " 受到 1 点普通伤害。");
}
}
// 2. 子类:史莱姆 (重写了受伤逻辑)
class Slime extends Monster {
public Slime() { super("史莱姆"); }
@Override
public void getHit() {
System.out.println("史莱姆身体变形了,只受到 0.5 点伤害!");
}
// 史莱姆特有技能
public void split() {
System.out.println("史莱姆分裂成了两个!");
}
}
// 3. 子类:巨龙 (重写了受伤逻辑)
class Dragon extends Monster {
public Dragon() { super("恶龙"); }
@Override
public void getHit() {
System.out.println("恶龙鳞片太硬,免疫了伤害!");
}
}
// 4. 游戏主逻辑
public class GameEngine {
// 【重点】参数写父类 Monster,可以接收所有子类!
// 这就是多态带来的“可扩展性”
public static void playerAttack(Monster m) {
System.out.println("--- 玩家发起攻击 ---");
// 动态绑定:自动调用 m 真实类型的方法
m.getHit();
// 如果是史莱姆,打它一下它会分裂
if (m instanceof Slime) {
Slime s = (Slime) m; // 向下转型
s.split();
}
}
public static void main(String[] args) {
Monster m1 = new Slime(); // 向上转型
Monster m2 = new Dragon(); // 向上转型
playerAttack(m1); // 传入史莱姆
playerAttack(m2); // 传入巨龙
}
}
运行结果:
Plaintext
--- 玩家发起攻击 ---
史莱姆身体变形了,只受到 0.5 点伤害!
史莱姆分裂成了两个!
--- 玩家发起攻击 ---
恶龙鳞片太硬,免疫了伤害!
总结:一句话记住
-
继承 (
extends ) :是为了复用代码,让子类拥有父类的属性和方法。 -
多态 (Polymorphism) :是为了解耦(降低代码依赖),写代码时只面对父类,运行时自动执行子类的逻辑。
-
口诀:
- 编译看左边(父类引用决定了能调哪些方法)。
- 运行看右边(子类对象决定了执行的具体逻辑)。
[github]("my/source/_posts/02-继承和多态.md at main · 1022260464/my · GitHub")