← Java 基础教程

第 22 / 40 课 · 免费教程

同一命令 不同攻击

用父类合同指挥不同对象

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

本课作品

制作混合战斗波次 BattleWave

指挥官只喊一次 attack,僵尸挥击、骷髅射箭、蜘蛛扑咬。跟着引用找到真实对象,你会看懂 Java 如何选中正确方法,并完成混合波次。 前置背包:第21课的 Enemy 家族、extends、super 和方法重写。今天让整支怪物波次听同一条命令。

开始前:建议先完成第 21 课

引用找到对象,对象提供重写行为
  1. #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. #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. #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 数组保存多种子类
  1. Enemy[]保存同一家族的引用
  2. 数组顺序决定出场顺序
  3. 动态分派决定攻击方式

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
蛛蛛 扑咬 伤害=5

05 / 13

方法参数使用父类合同

方法若只需要 Enemy 的能力,参数就写 Enemy,不必为每种子类重复一份方法。

一个入口服务整个敌人家族
  1. 参数 Enemy只依赖共同能力
  2. null 元素本课明确跳过
  3. 空数组总伤害为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
0

06 / 13

父类引用只看合同入口

即使实际对象是 Skeleton,Enemy 引用也只能直接调用 Enemy 中声明的成员。

对象可能会更多 引用只暴露 Enemy 成员
  1. attack / damage父类合同中已有
  2. reload仅骷髅特有
  3. 编译检查父类引用不能直接补箭

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。只有确实需要子类特有能力时才用,而且必须先检查。

先证明实际类型 再访问子类特有成员
  1. instanceof 检查是否确实是 Skeleton
  2. 模式变量 s匹配成功后可补箭
  3. 强制转型不匹配会抛异常

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);
    }
}

运行结果

小白 补充箭矢
false

BadCast.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

让对象代替类型分支

若主程序根据类型判断攻击方式,新增敌人时每个分支都要修改。重写让对象自己负责行为。

行为写进子类 循环只发送命令
  1. 加入 Witch新类实现攻击
  2. 旧循环不增加类型分支
  3. 扩展验证用输出证明兼容

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
蛛蛛 扑咬 伤害=5

09 / 13

波次模拟器测试台

波次程序要检查顺序、每种攻击、总伤害和错误输入。

混合对象 空波次和空元素都要验证
  1. 顺序测试按数组逐项出场
  2. 边界测试空数组和 null
  3. 总伤害各对象 damage 相加
验收对照表
场景输入预期
正常三种敌人按顺序输出三种攻击
边界空数组无输出 总伤害0
错误数组含null跳过 不崩溃
扩展加入Witch旧循环无需修改
回归总伤害等于各对象damage之和

10 / 13

挑战 蓝图统一波次

挑战 程序叫 BattleWave。三个子类分别返回攻击描述和伤害,两个通用方法负责播放波次与统计总伤害。

父类数组统一出场 攻击和伤害统计
  1. runWave播放每种攻击
  2. totalDamage只汇总数值
  3. Enemy 合同两方法共享同一入口

验收条件

  • 同一 Enemy 数组保存三种子类。
  • runWave 不出现 instanceof。
  • 每个对象输出自己的 attack。
  • totalDamage 返回15。
  • 空波次返回0,null 元素被安全跳过。

11 / 13

挑战 实现战斗波次

新建 BattleWave.java,在 Java 21 控制台运行。

一个循环触发三种实际行为
  1. 三种对象4、6、5点伤害
  2. null 槽位明确跳过
  3. 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
空波次=0

12 / 13

拓展任务与通关复盘

作业必须标出每个变量的声明类型和实际对象类型。

三档任务训练统一指挥
  1. 标声明类型例如 Enemy
  2. 标实际类型例如 Spider
  3. 标调用结果扑咬来自重写方法

三档作业

  • 木镐:用 Enemy 数组统一输出三种攻击。
  • 铁镐:给 Enemy 增加 loot 方法,用多态统一结算掉落。
  • 钻石镐:加入 Healer,让相同的 takeTurn 合同既能攻击也能治疗,并说明是否应改用接口。
验收对照表
自评能还要练
我理解父类引用□□
我能解释动态分派□□
我会避免类型分支□□

13 / 13

自己的验收记录

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

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

TAKE IT WITH YOU

下载本课 PDF

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