本课作品
制作敌人家族 EnemyFamily
三只怪物都要受伤,却各自有一套攻击方式。你会把共同规则放进父类,让子类只写自己的差异,再检查家族树有没有接错枝。 前置背包:类、对象、构造方法和封装。今天要把重复的生物代码搬进同一棵家族树。
开始前:建议先完成第 17–20 课
Enemy
| 子类 | 父类 | 关系 |
|---|---|---|
| Zombie | Enemy | 是一种敌人 |
| Skeleton | Enemy | 是一种敌人 |
| Creeper | Enemy | 是一种敌人 |
01 / 15
开始前:先预测,再让程序回答
本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。
每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。
终端命令:中文占位符换成实际文件名与类名
javac -encoding UTF-8 示例文件名.java
java 示例类名- 预测:先写预期输出,或画清引用和数据的变化。
- 验证:运行后逐行核对,再只改一个条件。
- 解释:说清结果来自 Java 规则、课程合同还是输入数据。
02 / 15
继承先回答是不是一种
僵尸、骷髅和苦力怕都有名字、生命和受伤行为。把这些共同代码复制三遍,修 Bug 时就要追三只怪物。
继承让子类复用父类的状态与行为,但前提是子类确实是父类的一种。
Enemy
| 子类 | 父类 | 关系 |
|---|---|---|
| Zombie | Enemy | 是一种敌人 |
| Skeleton | Enemy | 是一种敌人 |
| Creeper | Enemy | 是一种敌人 |
本课通关条件
- 读懂父类和子类的家族树。
- 让子类构造器调用 super。
- 使用 @Override 重写同名同参数方法。
- 为三种敌人准备正常、边界和错误测试。
03 / 15
父类保存共同部分
父类不是大杂物箱。只有所有敌人都需要、含义也一致的成员,才适合放进 Enemy。
- name名字只保存一份
- health私有生命字段
- 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());
}
}运行结果
守卫 生命=1304 / 15
extends 接上家族树
extends Enemy 表示 Skeleton 是 Enemy 的一种。它可以增加自己的字段和方法,也可以重写父类行为。
- extends声明是一种关系
- super初始化父类部分
- 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);
}
}运行结果
小白 射箭
true05 / 15
super 完成父类初始化
创建 Skeleton 时,Java 必须先初始化它的 Enemy 部分。super(...) 把名字和生命交给父类构造器。
- 父类构造器先完成共同状态
- 子类构造器再初始化专属字段
- 匹配参数不能凭空调用 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
64BadSuper.java · 故意错误示例
class Enemy { Enemy(String name) { } }
class Skeleton extends Enemy { Skeleton() { } }
public class BadSuper { public static void main(String[] args) { } }06 / 15
Override 重写攻击行为
父类提供通用攻击,子类可按自己的战斗方式重写。调用子类对象的 attack 时,执行子类版本。
- 方法签名名称和参数要匹配
- @Override编译器核对重写
- 实际对象决定实例方法版本
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
| 子类 | 父类 | 关系 |
|---|---|---|
| Zombie | Enemy | 挥击4 |
| Skeleton | Enemy | 射箭6 |
| Creeper | Enemy | 爆炸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
绿豆 爆炸 908 / 15
super 方法复用父类行为
有时子类不是完全替换父类行为,而是先复用,再补充自己的效果。
- super.attack()调用父类版本
- 补充效果在原结果后加击退
- 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 字段仍不能直接访问,应通过父类提供的方法维护规则。
- private health独立子类不能直接访问
- public 方法对外提供受控操作
- 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) { } }| 成员 | 建议权限 | 原因 |
|---|---|---|
| health | private | 禁止绕过生命规则 |
| name | protected 或 private | 按扩展需要选择 |
| takeDamage | 包内或 public | 提供受控行为 |
| attack | 包内或 public | 允许重写和调用 |
10 / 15
继承与组合如何选择
继承表达类型关系,组合表达对象拥有另一个对象。选错关系会让家族树长出奇怪树枝。
- Skeleton 是 Enemy继承连接类型
- Skeleton 有 Bow字段表达拥有
- 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
三类测试家族规则
继承测试既要检查子类差异,也要确认父类共同规则没有被破坏。
- 正常伤害生命按请求减少
- 负数伤害按合同保持原值
- 超大伤害生命最低为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
012 / 15
挑战 蓝图敌人家族
挑战 程序叫 EnemyFamily。父类保存名字和生命,三个子类重写 attack。完整程序还要证明父类受伤规则对所有子类有效。
- 父类保存共同状态
- 三个子类分别实现攻击
- 调用程序测试差异与共同规则
验收顺序
- 创建僵尸、骷髅和苦力怕。
- 依次输出三种不同攻击。
- 让骷髅受伤7点并输出状态。
- 给苦力怕负数伤害,状态必须不变。
- 确认三个子类都是 Enemy。
13 / 15
挑战 实现敌人家族
新建 EnemyFamily.java。这段程序只使用标准 Java 21,可直接编译运行。
- EnemyFamily.java辅助类和主类同一文件
- 一次一动作核对受伤与状态
- 继承证据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
true14 / 15
拓展任务与通关复盘
作业必须画出类图箭头,并写一句话证明每条继承关系符合是一种。
- 类图箭头从子类指向父类
- 测试表正常、边界、错误
- Bug 记录写出原因与最小修复
三档作业
- 木镐:创建 Zombie 和 Skeleton 两个子类,分别重写 attack。
- 铁镐:在父类增加 move,子类只重写真正不同的移动方式。
- 钻石镐:设计二阶段 挑战,第二阶段复用 super.attack 后追加特殊效果。
| 自评 | 能 | 还要练 |
|---|---|---|
| 我能判断继承关系 | □ | □ |
| 我会使用 super | □ | □ |
| 我会正确重写方法 | □ | □ |
15 / 15
自己的验收记录
漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。
| 类别 | 你选择的操作 | 核对重点 |
|---|---|---|
| 正常 | 一个常用输入或一次成功动作 | 结果是否逐行一致 |
| 边界 | 范围端点与相邻值 | 有没有多一步、少一项 |
| 错误 | 非法数量、缺失对象或坏数据 | 是否明确拒绝,状态是否保持 |
- Bug 记录:触发条件、实际现象、原因、最小修复。
- 图示记录:为每条箭头说明起点、终点和含义。
- 改编记录:只改一个规则,同时修改预期和测试。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。