← Java 基础教程

第 25 / 40 课 · 免费教程

会扩容的生物队列

用 ArrayList 编辑动态波次

90–110 分钟本章使用 JDK 21,无需预览功能。集合与程序实践

本课作品

制作动态波次编辑器 DynamicWave

波次编辑器要随时加怪、换怪、删怪。会扩容的列表给你自由,也要求你重新检查索引。你会用测试证明失败操作没有改坏队伍。 前置背包:数组、循环、方法和对象。数组像固定快捷栏,ArrayList 像会扩建的仓库,但管理员编号仍从0开始。

开始前:建议先完成第 15、21–24 课

两种容器都按顺序保存同类型元素
  1. 数组创建后长度固定
  2. ArrayList自动管理容量
  3. size()当前元素数

01 / 16

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

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

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

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

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

02 / 16

数组固定而列表会扩展

怪物波次数量在编辑时不断变化。数组需要预先决定长度,ArrayList 可以按需添加或删除。

两种容器都按顺序保存同类型元素
  1. 数组创建后长度固定
  2. ArrayList自动管理容量
  3. size()当前元素数
验收对照表
比较数组ArrayList
长度创建后固定可动态变化
读取items[i]list.get(i)
修改items[i] = xlist.set(i, x)
添加需另建数组list.add(x)
长度查询lengthsize()

03 / 16

泛型限制元素类型

泛型把错误挡在编译阶段。ArrayList<String> 表示列表元素必须是 String。

尖括号写明队列只收哪种物品
  1. List<String>元素类型在编译时检查
  2. ArrayList<>创建可变实现
  3. 泛型不能添加不兼容数值

L25Example02.java · 完整独立示例

import java.util.*;
public class L25Example02 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        wave.add("Zombie");
        System.out.println(wave);
    }
}

运行结果

[Zombie]

BadGeneric.java · 故意错误示例

import java.util.*;
public class BadGeneric {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        wave.add(20);
    }
}

04 / 16

add get 与 size

add 把元素放到末尾,size 返回当前元素数量。列表有3个元素时,合法索引为0、1、2。

添加到末尾 用索引读取 用 size 查询数量
  1. add(x)末尾添加
  2. add(i,x)插入并移动后续项
  3. get(i)读取合法索引

L25Example03.java · 完整独立示例

import java.util.*;
public class L25Example03 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        wave.add("Zombie"); wave.add("Skeleton");
        wave.add(1, "Creeper");
        System.out.println(wave);
        System.out.println(wave.size());
        System.out.println(wave.get(2));
    }
}

运行结果

[Zombie, Creeper, Skeleton]
3
Skeleton

05 / 16

set 与按索引 remove

set 替换指定元素,列表大小不变;remove(index) 删除元素,大小减1。

替换不改大小 删除会让后面元素左移
  1. set(i,x)替换,大小不变
  2. remove(i)删除,大小减1
  3. 右侧元素索引向左移动

L25Example04.java · 完整独立示例

import java.util.*;
public class L25Example04 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>(List.of("Zombie", "Skeleton"));
        System.out.println(wave.set(1, "Spider"));
        System.out.println(wave.remove(0));
        System.out.println(wave);
    }
}

运行结果

Skeleton
Zombie
[Spider]

06 / 16

删除索引还是删除对象

remove(1) 通常表示删除1号位置,remove("Spider") 表示删除首次匹配的对象。整数列表需要特别小心。

remove 重载根据参数类型选择版本
  1. remove(int)参数是索引
  2. remove(Object)删除首个匹配值
  3. Integer.valueOf明确选择对象删除

L25Example05.java · 完整独立示例

import java.util.*;
public class L25Example05 {
    public static void main(String[] args) {
        List<Integer> ids = new ArrayList<>(List.of(10, 20, 30));
        System.out.println(ids.remove(1));
        System.out.println(ids.remove(Integer.valueOf(10)));
        System.out.println(ids);
    }
}

运行结果

20
true
[30]

07 / 16

增强 for 读取队列

只想依次读取元素时,增强 for 最合适。遍历期间直接 add 或 remove 可能触发 ConcurrentModificationException。

只读遍历简洁清楚
  1. 增强 for适合顺序读取
  2. 结构修改会使迭代失效
  3. 受控删除用 removeIf 等方案

L25Example06.java · 完整独立示例

import java.util.*;
public class L25Example06 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>(List.of("Zombie", "Spider"));
        for (String mob : wave) System.out.println(mob);
    }
}

运行结果

Zombie
Spider

BadIteration.java · 故意错误示例

import java.util.*;
public class BadIteration {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>(List.of("A", "B", "C"));
        for (String mob : wave) wave.remove(mob);
    }
}

08 / 16

索引循环支持受控修改

替换指定位置时普通 for 很合适。若正向删除多个元素,删除后不要跳过刚移动过来的新元素。

需要位置时使用普通 for
  1. 倒序循环从最后一个索引开始
  2. 删除当前项不改变较小索引
  3. 不跳项相邻匹配也可删净

L25Example07.java · 完整独立示例

import java.util.*;
public class L25Example07 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>(List.of("Zombie", "Zombie", "Spider"));
        for (int i = wave.size() - 1; i >= 0; i--)
            if ("Zombie".equals(wave.get(i))) wave.remove(i);
        System.out.println(wave);
    }
}

运行结果

[Spider]

09 / 16

removeIf 安全过滤

按条件删除全部匹配时,removeIf 比手写索引循环更直接。

把删除条件交给列表统一执行
  1. removeIf按条件删除匹配项
  2. 常量.equals安全处理 null
  3. 可变列表删除的必要前提

L25Example08.java · 完整独立示例

import java.util.*;
public class L25Example08 {
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        wave.add(null); wave.add("Zombie"); wave.add("Spider");
        wave.removeIf("Zombie"::equals);
        System.out.println(wave);
    }
}

运行结果

[null, Spider]

10 / 16

空列表与索引边界

空列表可以正常存在,但 get(0) 会抛 IndexOutOfBoundsException。

size 为0时没有0号元素
  1. 空列表可以存在但不能 get(0)
  2. 合法索引0 <= i < size()
  3. size()不是最后一个索引

L25Example09.java · 完整独立示例

import java.util.*;
public class L25Example09 {
    static String safeGet(List<String> wave, int i) {
        return wave != null && i >= 0 && i < wave.size() ? wave.get(i) : null;
    }
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        System.out.println(safeGet(wave, 0));
        wave.add("Spider");
        System.out.println(safeGet(wave, 0));
        System.out.println(safeGet(wave, 1));
    }
}

运行结果

null
Spider
null
验收对照表
size合法索引说明
0无get(0)越界
10只有第一项
30到2最后是size减1

11 / 16

方法优先接收 List 合同

若方法只需要 List 的通用操作,参数类型可写 List<String>,调用者仍能传入 ArrayList。

调用者可选择不同列表实现
  1. 参数 List依赖通用合同
  2. 对象 ArrayList提供可变实现
  3. 返回 false非法请求不修改

L25Example10.java · 完整独立示例

import java.util.*;
public class L25Example10 {
    static boolean addMob(List<String> wave, String mob) {
        if (wave == null || mob == null || mob.isBlank()) return false;
        wave.add(mob.strip());
        return true;
    }
    static boolean replace(List<String> wave, int index, String mob) {
        if (wave == null || index < 0 || index >= wave.size()
                || mob == null || mob.isBlank()) return false;
        wave.set(index, mob.strip());
        return true;
    }
    static int removeAll(List<String> wave, String mob) {
        if (wave == null || mob == null || mob.isBlank()) return 0;
        String target = mob.strip();
        int before = wave.size();
        wave.removeIf(target::equals);
        return before - wave.size();
    }
    static void show(List<String> wave) {
        if (wave == null) { System.out.println("没有队列"); return; }
        for (int i = 0; i < wave.size(); i++)
            System.out.println(i + ": " + wave.get(i));
    }
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        System.out.println(addMob(wave, " Spider "));
        System.out.println(replace(wave, 1, "Zombie"));
        System.out.println(wave);
    }
}

运行结果

true
false
[Spider]

12 / 16

三类测试动态队列

动态容器改变频繁,测试要同时检查返回值、大小和元素顺序。

添加 替换 删除都验证状态
  1. 前快照复制元素序列
  2. 执行操作一次只测试一项
  3. 后比较返回值、大小、顺序
验收对照表
场景操作预期
正常添加Zombiesize增加1
正常替换索引1size不变 元素改变
边界删除最后一个成功且顺序正确
错误索引等于size返回false 数据不变
错误添加空名字返回false 数据不变

13 / 16

挑战 蓝图动态波次编辑器

挑战 程序叫 DynamicWave。它接收 List<String> 参数,主程序用 ArrayList 保存波次。

添加 替换 删除和显示组成编辑流程
  1. addMob只负责添加
  2. replace只负责替换
  3. removeAll按名字删除全部

验收顺序

  • 依次加入 Zombie、Skeleton、Zombie。
  • 把1号元素替换为 Spider,大小仍为3。
  • 删除全部 Zombie,应删除2只。
  • 最后显示0号位置为 Spider。
  • 非法索引和空名字返回 false,列表内容与顺序不变。

14 / 16

挑战 实现动态波次

新建 DynamicWave.java,使用 Java 21 编译运行。

泛型列表通过方法安全编辑
  1. 添加三项Zombie、Skeleton、Zombie
  2. 替换中间改为 Spider
  3. 过滤僵尸剩下一个 Spider

DynamicWave.java · 完整独立示例

import java.util.*;
public class DynamicWave {
static boolean addMob(List<String> wave, String mob) {
    if (wave == null || mob == null || mob.isBlank()) return false;
    wave.add(mob.strip());
    return true;
}
static boolean replace(List<String> wave, int index, String mob) {
    if (wave == null || index < 0 || index >= wave.size()
            || mob == null || mob.isBlank()) return false;
    wave.set(index, mob.strip());
    return true;
}
static int removeAll(List<String> wave, String mob) {
    if (wave == null || mob == null || mob.isBlank()) return 0;
    String target = mob.strip();
    int before = wave.size();
    wave.removeIf(target::equals);
    return before - wave.size();
}
static void show(List<String> wave) {
    if (wave == null) { System.out.println("没有队列"); return; }
    for (int i = 0; i < wave.size(); i++)
        System.out.println(i + ": " + wave.get(i));
}
    public static void main(String[] args) {
        List<String> wave = new ArrayList<>();
        addMob(wave, "Zombie"); addMob(wave, "Skeleton");
        addMob(wave, "Zombie");
        System.out.println("替换=" + replace(wave, 1, "Spider"));
        System.out.println("删除=" + removeAll(wave, "Zombie"));
        System.out.println("拒绝空名=" + !addMob(wave, " "));
        show(wave);
    }
}

运行结果

替换=true
删除=2
拒绝空名=true
0: Spider

15 / 16

拓展任务与单元复盘

第21至25课完成了继承、多态、接口、值对象和动态集合。作业同时提交代码、测试表和一次故障记录。

三档任务继续扩建数据仓库
  1. 类型设计继承、接口、值对象
  2. 容器操作泛型、索引、增删
  3. 交付证据代码与测试记录

三档作业

  • 木镐:用 ArrayList 管理任务清单,支持添加、完成和显示。
  • 铁镐:写 removeAllByName,删除全部同名生物并返回删除数量。
  • 钻石镐:用 Collections.shuffle 随机打乱波次,并保留一份原顺序副本用于比较。
验收对照表
自评能还要练
我会使用泛型列表□□
我能安全增删元素□□
我会测试列表边界□□

16 / 16

自己的验收记录

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

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

TAKE IT WITH YOU

下载本课 PDF

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