← Java 基础教程

第 21 / 40 课 · 免费教程

生物家族树

继承、super 与方法重写

90–110 分钟本章使用 JDK 21,无需预览功能。面向对象进阶

本课作品

制作敌人家族 EnemyFamily

三只怪物都要受伤,却各自有一套攻击方式。你会把共同规则放进父类,让子类只写自己的差异,再检查家族树有没有接错枝。 前置背包:类、对象、构造方法和封装。今天要把重复的生物代码搬进同一棵家族树。

开始前:建议先完成第 17–20 课

继承:空心三角指向父类

Enemy

子类父类关系
ZombieEnemy是一种敌人
SkeletonEnemy是一种敌人
CreeperEnemy是一种敌人

01 / 15

开始前:先预测,再让程序回答

本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。

每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。

终端命令:中文占位符换成实际文件名与类名

javac -encoding UTF-8 示例文件名.java
java 示例类名
  • 预测:先写预期输出,或画清引用和数据的变化。
  • 验证:运行后逐行核对,再只改一个条件。
  • 解释:说清结果来自 Java 规则、课程合同还是输入数据。

02 / 15

继承先回答是不是一种

僵尸、骷髅和苦力怕都有名字、生命和受伤行为。把这些共同代码复制三遍,修 Bug 时就要追三只怪物。

继承让子类复用父类的状态与行为,但前提是子类确实是父类的一种。

继承:空心三角指向父类

Enemy

子类父类关系
ZombieEnemy是一种敌人
SkeletonEnemy是一种敌人
CreeperEnemy是一种敌人

本课通关条件

  • 读懂父类和子类的家族树。
  • 让子类构造器调用 super。
  • 使用 @Override 重写同名同参数方法。
  • 为三种敌人准备正常、边界和错误测试。

03 / 15

父类保存共同部分

父类不是大杂物箱。只有所有敌人都需要、含义也一致的成员,才适合放进 Enemy。

共同字段与方法只维护一份
  1. name名字只保存一份
  2. health私有生命字段
  3. takeDamage统一受伤规则

L21Example02.java · 完整独立示例

class Enemy {
    protected final String name;
    private int health;
    Enemy(String name, int health) {
        this.name = name == null || name.isBlank()
            ? "未命名" : name.strip();
        this.health = Math.max(1, Math.min(40, health));
    }
    public void takeDamage(int amount) {
        if (amount > 0) health = Math.max(0, health - amount);
    }
    public int health() { return health; }
    public String attack() { return name + " 普通攻击 1"; }
    public String status() { return name + " 生命=" + health; }
}
public class L21Example02 {
    public static void main(String[] args) {
        Enemy e = new Enemy("守卫", 20);
        e.takeDamage(7);
        System.out.println(e.status());
    }
}

运行结果

守卫 生命=13

04 / 15

extends 接上家族树

extends Enemy 表示 Skeleton 是 Enemy 的一种。它可以增加自己的字段和方法,也可以重写父类行为。

子类自动拥有可继承的成员
  1. extends声明是一种关系
  2. super初始化父类部分
  3. attack子类可重写行为

L21Example03.java · 完整独立示例

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " 射箭"; }
}
public class L21Example03 {
    public static void main(String[] args) {
        Skeleton e = new Skeleton("小白");
        System.out.println(e.attack());
        System.out.println(e instanceof Enemy);
    }
}

运行结果

小白 射箭
true

05 / 15

super 完成父类初始化

创建 Skeleton 时,Java 必须先初始化它的 Enemy 部分。super(...) 把名字和生命交给父类构造器。

子类出生先建好父类部分
  1. 父类构造器先完成共同状态
  2. 子类构造器再初始化专属字段
  3. 匹配参数不能凭空调用 super()

L21Example04.java · 完整独立示例

class Enemy {
    protected final String name;
    private int health;
    Enemy(String name, int health) {
        this.name = name == null || name.isBlank()
            ? "未命名" : name.strip();
        this.health = Math.max(1, Math.min(40, health));
    }
    public void takeDamage(int amount) {
        if (amount > 0) health = Math.max(0, health - amount);
    }
    public int health() { return health; }
    public String attack() { return name + " 普通攻击 1"; }
    public String status() { return name + " 生命=" + health; }
}
class Skeleton extends Enemy {
    private int arrows;
    Skeleton(String name) { this(name, 12); }
    Skeleton(String name, int arrows) {
        super(name, 20);
        this.arrows = Math.max(0, Math.min(64, arrows));
    }
    public int arrows() { return arrows; }
    @Override public String attack() { return name + " 射箭 6"; }
}
public class L21Example04 {
    public static void main(String[] args) {
        Skeleton e = new Skeleton("小白", 99);
        System.out.println(e.status());
        System.out.println(e.arrows());
    }
}

运行结果

小白 生命=20
64

BadSuper.java · 故意错误示例

class Enemy { Enemy(String name) { } }
class Skeleton extends Enemy { Skeleton() { } }
public class BadSuper { public static void main(String[] args) { } }

06 / 15

Override 重写攻击行为

父类提供通用攻击,子类可按自己的战斗方式重写。调用子类对象的 attack 时,执行子类版本。

相同方法签名换成子类版本
  1. 方法签名名称和参数要匹配
  2. @Override编译器核对重写
  3. 实际对象决定实例方法版本

L21Example05.java · 完整独立示例

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " 射箭"; }
}
public class L21Example05 {
    public static void main(String[] args) {
        Enemy e = new Skeleton("小白");
        System.out.println(e.attack());
    }
}

运行结果

小白 射箭

BadOverride.java · 故意错误示例

class Enemy { public String attack() { return "普通攻击"; } }
class Skeleton extends Enemy {
    @Override public String attak() { return "射箭"; }
}
public class BadOverride { public static void main(String[] args) { } }

07 / 15

三种敌人三种攻击

三种敌人都继承 Enemy,但 attack 的文字和伤害值不同。差异留在子类,共同生命规则留在父类。

继承:空心三角指向父类

Enemy

子类父类关系
ZombieEnemy挥击4
SkeletonEnemy射箭6
CreeperEnemy爆炸9

L21Example06.java · 第1/2段,顺序接在同一文件

class Enemy {
    protected final String name;
    private int health;
    Enemy(String name, int health) {
        this.name = name == null || name.isBlank()
            ? "未命名" : name.strip();
        this.health = Math.max(1, Math.min(40, health));
    }
    public void takeDamage(int amount) {
        if (amount > 0) health = Math.max(0, health - amount);
    }
    public int health() { return health; }
    public String attack() { return name + " 普通攻击 1"; }
    public String status() { return name + " 生命=" + health; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name, 22); }
    @Override public String attack() { return name + " 挥击 4"; }
}
class Skeleton extends Enemy {
    private int arrows;
    Skeleton(String name) { this(name, 12); }
    Skeleton(String name, int arrows) {
        super(name, 20);
        this.arrows = Math.max(0, Math.min(64, arrows));
    }
    public int arrows() { return arrows; }
    @Override public String attack() { return name + " 射箭 6"; }

L21Example06.java · 第2/2段,顺序接在同一文件

}
class Creeper extends Enemy {
    Creeper(String name) { super(name, 18); }
    @Override public String attack() { return name + " 爆炸 9"; }
}
public class L21Example06 {
    public static void main(String[] args) {
        System.out.println(new Zombie("腐肉").attack());
        System.out.println(new Skeleton("小白").attack());
        System.out.println(new Creeper("绿豆").attack());
    }
}

运行结果

腐肉 挥击 4
小白 射箭 6
绿豆 爆炸 9

08 / 15

super 方法复用父类行为

有时子类不是完全替换父类行为,而是先复用,再补充自己的效果。

先执行父类版本 再增加子类效果
  1. super.attack()调用父类版本
  2. 补充效果在原结果后加击退
  3. attack()无条件自调用会递归

L21Example07.java · 完整独立示例

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " 普通攻击"; }
}
class ArmoredZombie extends Enemy {
    ArmoredZombie(String name) { super(name); }
    @Override public String attack() {
        return super.attack() + " + 击退";
    }
}
public class L21Example07 {
    public static void main(String[] args) {
        System.out.println(new ArmoredZombie("铁甲").attack());
    }
}

运行结果

铁甲 普通攻击 + 击退

09 / 15

访问权限守住边界

子类不是父类的万能钥匙。父类 private 字段仍不能直接访问,应通过父类提供的方法维护规则。

子类可扩展但不能拆掉父类保险柜
  1. private health独立子类不能直接访问
  2. public 方法对外提供受控操作
  3. protected name允许子类读取名字

BadHealth.java · 故意错误示例

class Enemy {
    private int health = 20;
    public int health() { return health; }
}
class Skeleton extends Enemy {
    void cheat() { health = 999; }
}
public class BadHealth { public static void main(String[] args) { } }
验收对照表
成员建议权限原因
healthprivate禁止绕过生命规则
nameprotected 或 private按扩展需要选择
takeDamage包内或 public提供受控行为
attack包内或 public允许重写和调用

10 / 15

继承与组合如何选择

继承表达类型关系,组合表达对象拥有另一个对象。选错关系会让家族树长出奇怪树枝。

是一种用继承 有一个用组合
  1. Skeleton 是 Enemy继承连接类型
  2. Skeleton 有 Bow字段表达拥有
  3. Bow 不是 Enemy不要接错家族树
验收对照表
问题答案是是选择
Skeleton 是 Enemy 吗是继承
Skeleton 是 Bow 吗否不要继承
Skeleton 有 Bow 吗是组合
挑战 是 Enemy 吗通常是可继承

L21Example09.java · 完整独立示例

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " 普通攻击"; }
}
class Bow { String shoot() { return "拉弓"; } }
class Skeleton extends Enemy {
    private final Bow bow = new Bow();
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " " + bow.shoot(); }
}
public class L21Example09 {
    public static void main(String[] args) {
        Skeleton e = new Skeleton("小白");
        System.out.println(e.attack());
    }
}

运行结果

小白 拉弓

11 / 15

三类测试家族规则

继承测试既要检查子类差异,也要确认父类共同规则没有被破坏。

构造 攻击 受伤都要检查边界
  1. 正常伤害生命按请求减少
  2. 负数伤害按合同保持原值
  3. 超大伤害生命最低为0
验收对照表
场景操作预期
正常Skeleton attack输出射箭6
正常Zombie 受伤5生命减少5
边界受到超大伤害生命停在0
错误负数伤害生命不变
回归三个子类 status格式一致

L21Example10.java · 完整独立示例

class Enemy {
    protected final String name;
    private int health;
    Enemy(String name, int health) {
        this.name = name == null || name.isBlank()
            ? "未命名" : name.strip();
        this.health = Math.max(1, Math.min(40, health));
    }
    public void takeDamage(int amount) {
        if (amount > 0) health = Math.max(0, health - amount);
    }
    public int health() { return health; }
    public String attack() { return name + " 普通攻击 1"; }
    public String status() { return name + " 生命=" + health; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name, 22); }
    @Override public String attack() { return name + " 挥击 4"; }
}
public class L21Example10 {
    public static void main(String[] args) {
        Enemy e = new Zombie("腐肉");
        e.takeDamage(-5);
        System.out.println(e.health());
        e.takeDamage(Integer.MAX_VALUE);
        System.out.println(e.health());
    }
}

运行结果

22
0

12 / 15

挑战 蓝图敌人家族

挑战 程序叫 EnemyFamily。父类保存名字和生命,三个子类重写 attack。完整程序还要证明父类受伤规则对所有子类有效。

父类管共同状态 子类管攻击差异
  1. 父类保存共同状态
  2. 三个子类分别实现攻击
  3. 调用程序测试差异与共同规则

验收顺序

  • 创建僵尸、骷髅和苦力怕。
  • 依次输出三种不同攻击。
  • 让骷髅受伤7点并输出状态。
  • 给苦力怕负数伤害,状态必须不变。
  • 确认三个子类都是 Enemy。

13 / 15

挑战 实现敌人家族

新建 EnemyFamily.java。这段程序只使用标准 Java 21,可直接编译运行。

完整程序验证 super 重写和父类规则
  1. EnemyFamily.java辅助类和主类同一文件
  2. 一次一动作核对受伤与状态
  3. 继承证据instanceof 为 true

EnemyFamily.java · 第1/2段,顺序接在同一文件

class Enemy {
    protected final String name;
    private int health;
    Enemy(String name, int health) {
        this.name = name == null || name.isBlank()
            ? "未命名" : name.strip();
        this.health = Math.max(1, Math.min(40, health));
    }
    public void takeDamage(int amount) {
        if (amount > 0) health = Math.max(0, health - amount);
    }
    public int health() { return health; }
    public String attack() { return name + " 普通攻击 1"; }
    public String status() { return name + " 生命=" + health; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name, 22); }
    @Override public String attack() { return name + " 挥击 4"; }
}
class Skeleton extends Enemy {
    private int arrows;
    Skeleton(String name) { this(name, 12); }
    Skeleton(String name, int arrows) {
        super(name, 20);
        this.arrows = Math.max(0, Math.min(64, arrows));
    }
    public int arrows() { return arrows; }
    @Override public String attack() { return name + " 射箭 6"; }

EnemyFamily.java · 第2/2段,顺序接在同一文件

}
class Creeper extends Enemy {
    Creeper(String name) { super(name, 18); }
    @Override public String attack() { return name + " 爆炸 9"; }
}
public class EnemyFamily {
    public static void main(String[] args) {
        Zombie zombie = new Zombie("腐肉队长");
        Skeleton skeleton = new Skeleton("小白");
        Creeper creeper = new Creeper("绿豆");
        System.out.println(zombie.attack());
        System.out.println(skeleton.attack());
        System.out.println(creeper.attack());
        skeleton.takeDamage(7);
        creeper.takeDamage(-3);
        System.out.println(skeleton.status());
        System.out.println(creeper.status());
        System.out.println(skeleton instanceof Enemy);
    }
}

运行结果

腐肉队长 挥击 4
小白 射箭 6
绿豆 爆炸 9
小白 生命=13
绿豆 生命=18
true

14 / 15

拓展任务与通关复盘

作业必须画出类图箭头,并写一句话证明每条继承关系符合是一种。

三档任务继续扩建家族树
  1. 类图箭头从子类指向父类
  2. 测试表正常、边界、错误
  3. Bug 记录写出原因与最小修复

三档作业

  • 木镐:创建 Zombie 和 Skeleton 两个子类,分别重写 attack。
  • 铁镐:在父类增加 move,子类只重写真正不同的移动方式。
  • 钻石镐:设计二阶段 挑战,第二阶段复用 super.attack 后追加特殊效果。
验收对照表
自评能还要练
我能判断继承关系□□
我会使用 super□□
我会正确重写方法□□

15 / 15

自己的验收记录

漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。

为判断留下依据
类别你选择的操作核对重点
正常一个常用输入或一次成功动作结果是否逐行一致
边界范围端点与相邻值有没有多一步、少一项
错误非法数量、缺失对象或坏数据是否明确拒绝,状态是否保持
  • Bug 记录:触发条件、实际现象、原因、最小修复。
  • 图示记录:为每条箭头说明起点、终点和含义。
  • 改编记录:只改一个规则,同时修改预期和测试。

TAKE IT WITH YOU

下载本课 PDF

完整课文、代码、图解与练习,选择你需要的语言。