本课作品
制作混合战斗波次 BattleWave
指挥官只喊一次 attack,僵尸挥击、骷髅射箭、蜘蛛扑咬。跟着引用找到真实对象,你会看懂 Java 如何选中正确方法,并完成混合波次。 前置背包:第21课的 Enemy 家族、extends、super 和方法重写。今天让整支怪物波次听同一条命令。
开始前:建议先完成第 21 课
- #1 · Skeleton 对象attack() 返回射箭描述
| 变量 | 引用指向 |
|---|---|
| Enemy enemy | #1 |
01 / 13
开始前:先预测,再让程序回答
本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。
每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。
终端命令:中文占位符换成实际文件名与类名
javac -encoding UTF-8 示例文件名.java
java 示例类名- 预测:先写预期输出,或画清引用和数据的变化。
- 验证:运行后逐行核对,再只改一个条件。
- 解释:说清结果来自 Java 规则、课程合同还是输入数据。
02 / 13
父类引用容纳子类对象
Enemy enemy = new Skeleton(...) 左边说明程序通过 Enemy 合同使用它,右边决定真正创建的是 Skeleton。
- #1 · Skeleton 对象attack() 返回射箭描述
| 变量 | 引用指向 |
|---|---|
| Enemy enemy | #1 |
L22Example01.java · 完整独立示例
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
public String reload() { return name + " 补充箭矢"; }
Skeleton(String name) { super(name); }
@Override public String attack() { return name + " 射箭"; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
}
public class L22Example01 {
public static void main(String[] args) {
Enemy a = new Zombie("腐肉");
Enemy b = new Skeleton("小白");
System.out.println(a.attack());
System.out.println(b.attack());
}
}运行结果
腐肉 挥击
小白 射箭03 / 13
动态分派选择实际方法
Java 对实例方法使用动态分派。编译器先确认 Enemy 合同里有 attack,运行时再执行实际子类的重写版本。
- #1 · Skeleton 对象attack() 返回射箭描述
| 变量 | 引用指向 |
|---|---|
| Enemy enemy | #1 |
L22Example02.java · 完整独立示例
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
public String reload() { return name + " 补充箭矢"; }
Skeleton(String name) { super(name); }
@Override public String attack() { return name + " 射箭"; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
}
public class L22Example02 {
public static void main(String[] args) {
Enemy e = new Skeleton("小白");
System.out.println(e.attack());
e = new Zombie("腐肉");
System.out.println(e.attack());
}
}运行结果
小白 射箭
腐肉 挥击04 / 13
父类数组装下混合波次
数组元素类型写 Enemy,就能把所有 Enemy 子类放在同一波次中。
- Enemy[]保存同一家族的引用
- 数组顺序决定出场顺序
- 动态分派决定攻击方式
L22Example03.java · 第1/2段,顺序接在同一文件
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
public int damage() { return 1; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
@Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
Skeleton(String name) { super(name); }
public String reload() { return name + " 补充箭矢"; }
@Override public String attack() { return name + " 射箭"; }
@Override public int damage() { return 6; }
}
class Spider extends Enemy {
Spider(String name) { super(name); }
@Override public String attack() { return name + " 扑咬"; }
@Override public int damage() { return 5; }
}
public class L22Example03 {
static void runWave(Enemy[] wave) {
if (wave == null) return;
for (Enemy enemy : wave)
if (enemy != null)
System.out.println(enemy.attack() + " 伤害=" + enemy.damage());L22Example03.java · 第2/2段,顺序接在同一文件
}
static long totalDamage(Enemy[] wave) {
if (wave == null) return 0;
long total = 0;
for (Enemy enemy : wave)
if (enemy != null) total += enemy.damage();
return total;
}
public static void main(String[] args) {
Enemy[] wave = {new Zombie("腐肉"), new Skeleton("小白"),
new Spider("蛛蛛")};
runWave(wave);
}
}运行结果
腐肉 挥击 伤害=4
小白 射箭 伤害=6
蛛蛛 扑咬 伤害=505 / 13
方法参数使用父类合同
方法若只需要 Enemy 的能力,参数就写 Enemy,不必为每种子类重复一份方法。
- 参数 Enemy只依赖共同能力
- null 元素本课明确跳过
- 空数组总伤害为0
L22Example04.java · 第1/2段,顺序接在同一文件
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
public int damage() { return 1; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
@Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
Skeleton(String name) { super(name); }
public String reload() { return name + " 补充箭矢"; }
@Override public String attack() { return name + " 射箭"; }
@Override public int damage() { return 6; }
}
class Spider extends Enemy {
Spider(String name) { super(name); }
@Override public String attack() { return name + " 扑咬"; }
@Override public int damage() { return 5; }
}
public class L22Example04 {
static void runWave(Enemy[] wave) {
if (wave == null) return;
for (Enemy enemy : wave)
if (enemy != null)
System.out.println(enemy.attack() + " 伤害=" + enemy.damage());L22Example04.java · 第2/2段,顺序接在同一文件
}
static long totalDamage(Enemy[] wave) {
if (wave == null) return 0;
long total = 0;
for (Enemy enemy : wave)
if (enemy != null) total += enemy.damage();
return total;
}
public static void main(String[] args) {
runWave(new Enemy[]{null, new Skeleton("小白")});
runWave(null);
System.out.println(totalDamage(new Enemy[0]));
}
}运行结果
小白 射箭 伤害=6
006 / 13
父类引用只看合同入口
即使实际对象是 Skeleton,Enemy 引用也只能直接调用 Enemy 中声明的成员。
- attack / damage父类合同中已有
- reload仅骷髅特有
- 编译检查父类引用不能直接补箭
BadReload.java · 故意错误示例
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
public String reload() { return name + " 补充箭矢"; }
Skeleton(String name) { super(name); }
@Override public String attack() { return name + " 射箭"; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
}
public class BadReload {
public static void main(String[] args) {
Enemy e = new Skeleton("小白");
System.out.println(e.reload());
}
}07 / 13
向下转型要有限且安全
向下转型把 Enemy 视为更具体的 Skeleton。只有确实需要子类特有能力时才用,而且必须先检查。
- instanceof 检查是否确实是 Skeleton
- 模式变量 s匹配成功后可补箭
- 强制转型不匹配会抛异常
L22Example06.java · 完整独立示例
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
public String reload() { return name + " 补充箭矢"; }
Skeleton(String name) { super(name); }
@Override public String attack() { return name + " 射箭"; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
}
public class L22Example06 {
public static void main(String[] args) {
Enemy e = new Skeleton("小白");
if (e instanceof Skeleton s) System.out.println(s.reload());
e = new Zombie("腐肉");
System.out.println(e instanceof Skeleton);
}
}运行结果
小白 补充箭矢
falseBadCast.java · 故意错误示例
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
}
class Skeleton extends Enemy {
public String reload() { return name + " 补充箭矢"; }
Skeleton(String name) { super(name); }
@Override public String attack() { return name + " 射箭"; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
}
public class BadCast {
public static void main(String[] args) {
Enemy e = new Zombie("腐肉");
Skeleton s = (Skeleton) e;
System.out.println(s.reload());
}
}08 / 13
让对象代替类型分支
若主程序根据类型判断攻击方式,新增敌人时每个分支都要修改。重写让对象自己负责行为。
- 加入 Witch新类实现攻击
- 旧循环不增加类型分支
- 扩展验证用输出证明兼容
L22Example07.java · 第1/2段,顺序接在同一文件
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
public int damage() { return 1; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
@Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
Skeleton(String name) { super(name); }
public String reload() { return name + " 补充箭矢"; }
@Override public String attack() { return name + " 射箭"; }
@Override public int damage() { return 6; }
}
class Spider extends Enemy {
Spider(String name) { super(name); }
@Override public String attack() { return name + " 扑咬"; }
@Override public int damage() { return 5; }
}
class Witch extends Enemy {
Witch(String name) { super(name); }
@Override public String attack() { return name + " 投掷药水"; }
@Override public int damage() { return 7; }
}
public class L22Example07 {L22Example07.java · 第2/2段,顺序接在同一文件
static void runWave(Enemy[] wave) {
if (wave == null) return;
for (Enemy enemy : wave)
if (enemy != null)
System.out.println(enemy.attack() + " 伤害=" + enemy.damage());
}
static long totalDamage(Enemy[] wave) {
if (wave == null) return 0;
long total = 0;
for (Enemy enemy : wave)
if (enemy != null) total += enemy.damage();
return total;
}
public static void main(String[] args) {
runWave(new Enemy[]{new Witch("紫袍"), new Spider("蛛蛛")});
}
}运行结果
紫袍 投掷药水 伤害=7
蛛蛛 扑咬 伤害=509 / 13
波次模拟器测试台
波次程序要检查顺序、每种攻击、总伤害和错误输入。
- 顺序测试按数组逐项出场
- 边界测试空数组和 null
- 总伤害各对象 damage 相加
| 场景 | 输入 | 预期 |
|---|---|---|
| 正常 | 三种敌人 | 按顺序输出三种攻击 |
| 边界 | 空数组 | 无输出 总伤害0 |
| 错误 | 数组含null | 跳过 不崩溃 |
| 扩展 | 加入Witch | 旧循环无需修改 |
| 回归 | 总伤害 | 等于各对象damage之和 |
10 / 13
挑战 蓝图统一波次
挑战 程序叫 BattleWave。三个子类分别返回攻击描述和伤害,两个通用方法负责播放波次与统计总伤害。
- runWave播放每种攻击
- totalDamage只汇总数值
- Enemy 合同两方法共享同一入口
验收条件
- 同一 Enemy 数组保存三种子类。
- runWave 不出现 instanceof。
- 每个对象输出自己的 attack。
- totalDamage 返回15。
- 空波次返回0,null 元素被安全跳过。
11 / 13
挑战 实现战斗波次
新建 BattleWave.java,在 Java 21 控制台运行。
- 三种对象4、6、5点伤害
- null 槽位明确跳过
- long 总数得到15
BattleWave.java · 第1/2段,顺序接在同一文件
class Enemy {
protected final String name;
Enemy(String name) { this.name = name; }
public String attack() { return name + " 普通攻击"; }
public int damage() { return 1; }
}
class Zombie extends Enemy {
Zombie(String name) { super(name); }
@Override public String attack() { return name + " 挥击"; }
@Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
Skeleton(String name) { super(name); }
public String reload() { return name + " 补充箭矢"; }
@Override public String attack() { return name + " 射箭"; }
@Override public int damage() { return 6; }
}
class Spider extends Enemy {
Spider(String name) { super(name); }
@Override public String attack() { return name + " 扑咬"; }
@Override public int damage() { return 5; }
}
public class BattleWave {
static void runWave(Enemy[] wave) {
if (wave == null) return;
for (Enemy enemy : wave)
if (enemy != null)
System.out.println(enemy.attack() + " 伤害=" + enemy.damage());BattleWave.java · 第2/2段,顺序接在同一文件
}
static long totalDamage(Enemy[] wave) {
if (wave == null) return 0;
long total = 0;
for (Enemy enemy : wave)
if (enemy != null) total += enemy.damage();
return total;
}
public static void main(String[] args) {
Enemy[] wave = {new Zombie("腐肉"), new Skeleton("小白"),
new Spider("蛛蛛"), null};
runWave(wave);
System.out.println("总伤害=" + totalDamage(wave));
System.out.println("空波次=" + totalDamage(new Enemy[0]));
}
}运行结果
腐肉 挥击 伤害=4
小白 射箭 伤害=6
蛛蛛 扑咬 伤害=5
总伤害=15
空波次=012 / 13
拓展任务与通关复盘
作业必须标出每个变量的声明类型和实际对象类型。
- 标声明类型例如 Enemy
- 标实际类型例如 Spider
- 标调用结果扑咬来自重写方法
三档作业
- 木镐:用 Enemy 数组统一输出三种攻击。
- 铁镐:给 Enemy 增加 loot 方法,用多态统一结算掉落。
- 钻石镐:加入 Healer,让相同的 takeTurn 合同既能攻击也能治疗,并说明是否应改用接口。
| 自评 | 能 | 还要练 |
|---|---|---|
| 我理解父类引用 | □ | □ |
| 我能解释动态分派 | □ | □ |
| 我会避免类型分支 | □ | □ |
13 / 13
自己的验收记录
漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。
| 类别 | 你选择的操作 | 核对重点 |
|---|---|---|
| 正常 | 一个常用输入或一次成功动作 | 结果是否逐行一致 |
| 边界 | 范围端点与相邻值 | 有没有多一步、少一项 |
| 错误 | 非法数量、缺失对象或坏数据 | 是否明确拒绝,状态是否保持 |
- Bug 记录:触发条件、实际现象、原因、最小修复。
- 图示记录:为每条箭头说明起点、终点和含义。
- 改编记录:只改一个规则,同时修改预期和测试。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。