java学习笔记-02继承和多态

7 阅读8分钟

02继承和多态

  • 认识继承
  • 权限修饰符
  • 继承的特点
  • 方法重写
  • 子类构造器的特点

Java OOP 核心笔记:继承与多态


# Java 面向对象核心笔记:单继承详解

1. 认识继承 (Inheritance)

1.1 什么是继承?

继承是 Java 面向对象编程(OOP)的三大特征之一(封装、继承、多态)。它允许我们创建一个新的类(子类/派生类),从已有的类(父类/超类/基类)那里直接获得属性和方法。

  • 核心关键字​:extends
  • 格式
  • 本质逻辑​:is-a 关系。例如:Student is a Person​(学生是一个人),Dog is an Animal(狗是一种动物)。

1.2 继承的好处

  1. 代码复用:父类写一次,所有子类都能用。
  2. 便于维护:修改父类逻辑,所有子类自动同步更新。
  3. 多态的前提:没有继承就没有多态。

1.3 内存中的继承(重要)

当创建一个子类对象时,内存中会发生什么?

  • 在堆内存(Heap)中,子类对象其实包含了一个父类对象的“分身”。
  • 即:子类对象 = 父类的部分 + 子类特有的部分。

2. 权限修饰符 (Access Modifiers)

在继承关系中,最重要的就是搞清楚:爸爸的东西,儿子到底能不能用?

Java 有四种权限修饰符,按访问能力从大到小排列如下:

修饰符关键字本类中本包中 (子类/无关类)不同包的子类不同包的无关类
公共public
受保护protected✅ (重点)
默认(default)
私有private

继承中的特殊说明:

  1. private(私有)

    • 能继承吗? 严格来说,​能继承(子类对象内存里确实有这个字段)。
    • 能访问吗?不能直接访问​。儿子拥有父亲的私房钱,但父亲锁在保险柜里,儿子打不开。必须通过父类提供的 public​ 的 get/set 方法来间接访问。
  2. 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 必须遵守的规则 (重点面试题)

我们可以总结为: “两同、两小、一大”

  1. 两同

    • 方法名必须相同。
    • 参数列表必须相同。
  2. 一大 (权限)

    • 子类方法的访问权限 \ge 父类方法的访问权限
    • 例子:父类是 protected​,子类可以是 protected​ 或 public​,但不能是 private(不能越活越回去)。(不能越权访问,等级森严这一块)
  3. 两小 (返回值与异常)

    • 返回值类型​:子类方法的返回值类型,必须与父类相同,或者是父类返回类型的​子类
    • 抛出的异常:子类抛出的异常不能比父类更多(只能是父类异常的子类或不抛出)。

4.3 核心注意事项

  • @Override注解:强烈建议写上!它能帮编译器检查你是不是写错了(比如名字打错字母),起到安全校验的作用。
  • 私有方法不能重写:父类的 private 方法对子类不可见,子类就算写了一个一模一样的,那也不叫重写,那叫子类自己新定义了一个方法。

!!!实际开发过程当中,直接写一样的返回类型就好了!!!访问权限可以更大,但一般都是使用public和private


5. 子类构造器的特点 (Constructor)

这是继承中最容易出错的地方。​构造器是不能被继承的​(你不能说儿子直接继承了爸爸的名字),但子类初始化时必须调用父类的构造器。

5.1 核心原则

子类构造器执行时,必须先执行父类的构造器。

为什么?因为子类继承了父类的数据,在子类初始化之前,必须先让父类把数据初始化好(先有爸爸,才有儿子)。

5.2 代码执行流程

  1. 隐式调用 (super()) ​:
    如果你在子类构造器里什么都不写,Java 编译器会默认在第一行加上 super();​,即调用父类的​无参构造器

  2. 显式调用 (super(参数)) ​:
    如果父类​没有无参构造器​(只写了带参构造器),那么子类必须在构造器的​第一行​,手动写上 super(参数) 来调用父类的带参构造器,否则编译报错。

  3. this(...) super(...)的冲突

    • super(...) 必须在第一行。
    • this(...) (调用本类其他构造器) 也必须在第一行。
    • 结论:在同一个构造器中,super(...)​ 和 this(...)不能同时出现
  4. this(...)调用兄弟构造器 的核心要点,可以把它看作是 “构造器内部的外包机制”

    1. 核心定义

    • 含义:在同一个类中,一个构造器去调用另一个构造器。
    • 目的复用代码(DRY 原则)。防止在多个构造器里写重复的初始化代码(如 this.name = name 写好几遍)。

    2. 最佳实践模式:漏斗模式 (The Funnel)

    “小构造器” 调用 “大构造器”。

    • 大构造器(全参):负责真正干活,包含所有的初始化逻辑。
    • 小构造器(无参/少参):只负责给默认值,然后把活儿外包给大构造器。

    3. 三大铁律 (死规矩)

    1. 位置霸道this(...)​ 必须写在构造器的 第一行(和 super 一样霸道)。
    2. 一山不容二虎:同一个构造器里,不能同时出现 this(...)​ 和 super(...)。(因为都争第一行)。
    3. 禁止套娃:严禁递归调用(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. 什么是多态?

字面意思:一种事物,多种形态。

代码意思​:父类的引用,指向了子类的对象。

这是多态的终极公式:

Parentobj=newChild();\text{Parent} \quad \text{obj} = \text{new} \quad \text{Child}();

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. 多态的三大前提

要想实现多态,缺一不可:

  1. 要有继承关系 (Warrior extends Hero)
  2. 要有方法重写 (子类重写了 attack 方法)
  3. 父类引用指向子类对象 (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("这是一个法师");
    }
}

第四部分:综合实战案例 —— 简易游戏引擎

让我们用一个完整的案例把继承、重写、多态、向上转型整合起来。

需求

  1. 有一个 GameEngine(游戏引擎)。
  2. 它可以接收任何类型的怪物 Monster
  3. 不论传入的是 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 点伤害!
史莱姆分裂成了两个!
--- 玩家发起攻击 ---
恶龙鳞片太硬,免疫了伤害!

总结:一句话记住

  1. 继承 (extends) ​:是为了​复用代码,让子类拥有父类的属性和方法。

  2. 多态 (Polymorphism) ​:是为了​解耦(降低代码依赖),写代码时只面对父类,运行时自动执行子类的逻辑。

  3. 口诀

    • 编译看左边(父类引用决定了能调哪些方法)。
    • 运行看右边(子类对象决定了执行的具体逻辑)。

[github]("my/source/_posts/02-继承和多态.md at main · 1022260464/my · GitHub") ‍