本课作品
制作玩家计分板 ScoreBoard
服务器里来了新玩家,计分板无需再添一个变量。以名字为键,读出分数,再用明确规则处理缺失、扣分、排序与整数上限。 前置背包:变量、方法、循环、ArrayList 和泛型。今天让第100位玩家加入时也不必创建 player100Score。
开始前:建议先完成第 25 课
- key玩家名,唯一查找标识
- value分数,可重复
- entry一条键值映射
01 / 14
开始前:先预测,再让程序回答
本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。
每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。
终端命令:中文占位符换成实际文件名与类名
javac -encoding UTF-8 示例文件名.java
java 示例类名- 预测:先写预期输出,或画清引用和数据的变化。
- 验证:运行后逐行核对,再只改一个条件。
- 解释:说清结果来自 Java 规则、课程合同还是输入数据。
02 / 14
键和值组成查找表
Map 像服务器计分板。玩家名字是键,分数是值;程序通过名字快速找到对应分数。
同一个键只保留一份当前值。两个不同玩家可以恰好拥有相同分数。
- key玩家名,唯一查找标识
- value分数,可重复
- entry一条键值映射
| 概念 | 计分板例子 | 规则 |
|---|---|---|
| 键 key | 玩家名 Alex | 在同一 Map 中唯一 |
| 值 value | 分数 12 | 允许重复 |
| 映射 entry | Alex 对应 12 | 一组键和值 |
03 / 14
创建 HashMap 并放入数据
声明时写出键和值的类型。尖括号中的 String, Integer 表示只能使用字符串键和整数值。
- Map<String,Integer>明确键值类型
- put同名键替换旧值
- size当前记录数量
L26Example02.java · 完整独立示例
import java.util.*;
public class L26Example02 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alex", 12); scores.put("Steve", 12);
scores.put("Alex", 20);
System.out.println(scores.get("Alex"));
System.out.println(scores.size());
}
}运行结果
20
2BadMapType.java · 故意错误示例
import java.util.*;
public class BadMapType {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alex", "12");
}
}04 / 14
安全累加分数
同名 put 会替换旧值,记录数量不增加。新键的 get 返回 null,直接拆箱相加会失败。先看简单计数,再给大数、null 与非法旧记录建立明确合同。
- 不存在getOrDefault 给默认0
- 已存在 null默认值不会代替它
- 安全加法大数先转 long 检查
L26Example03.java · 完整独立示例
import java.util.*;
public class L26Example03 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alex", scores.getOrDefault("Alex", 0) + 5);
scores.merge("Alex", 3, Integer::sum);
System.out.println(scores.get("Alex"));
}
}运行结果
805 / 14
不存在的键要明确处理
get 对不存在的键返回 null。计分板可以选择显示0,也可以明确告诉用户找不到玩家。
- get缺失键返回 null
- containsKey判断键是否存在
- 0分玩家存在与缺失不同
L26Example04.java · 完整独立示例
import java.util.*;
public class L26Example04 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alex", 0);
System.out.println(scores.get("Mia"));
System.out.println(scores.getOrDefault("Mia", 0));
System.out.println(scores.containsKey("Alex"));
System.out.println(scores.containsKey("Mia"));
}
}运行结果
null
0
true
false06 / 14
entrySet 一次拿到键和值
只遍历 keySet 再调用 get 也能工作,但 entrySet 直接提供完整记录,表达更清楚。
- entrySet同时拿到键与值
- HashMap不承诺遍历顺序
- TreeMap 副本按键生成稳定展示
L26Example05.java · 完整独立示例
import java.util.*;
public class L26Example05 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>(Map.of("Steve", 8, "Alex", 12));
for (var entry : new TreeMap<>(scores).entrySet())
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}运行结果
Alex=12
Steve=807 / 14
删除玩家并理解顺序
remove(key) 删除整条记录,并返回旧值;键不存在时返回 null。clear() 会清空整张表。
- remove(key)删除并返回旧值
- clear()清空全部记录
- 展示顺序需明确选择实现
L26Example06.java · 完整独立示例
import java.util.*;
public class L26Example06 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>(Map.of("Alex", 12));
System.out.println(scores.remove("Alex"));
System.out.println(scores.remove("Alex"));
scores.clear();
System.out.println(scores.size());
}
}运行结果
12
null
008 / 14
方法参数面向 Map 合同
方法若只需要 Map 的通用操作,参数应写 Map<K,V>,而不是锁死为 HashMap。
- 名字检查拒绝空名,去首尾空白
- long 计算防止 int 相加溢出
- 更新最后拒绝时数据不变
L26Example07.java · 完整独立示例
import java.util.*;
public class L26Example07 {
static boolean addScore(Map<String, Integer> scores,
String player, int delta) {
if (scores == null || player == null || player.isBlank()) return false;
String key = player.strip();
Integer old = scores.get(key);
if (scores.containsKey(key) && (old == null || old < 0)) return false;
int current = old == null ? 0 : old;
long next = (long) current + delta;
if (next > Integer.MAX_VALUE) return false;
scores.put(key, (int) Math.max(0L, next));
return true;
}
static int getScore(Map<String, Integer> scores, String player) {
if (scores == null || player == null || player.isBlank()) return 0;
String key = player.strip();
Integer value = scores.get(key);
if (!scores.containsKey(key)) return 0;
if (value == null || value < 0)
throw new IllegalStateException("计分记录损坏:" + key);
return value;
}
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
System.out.println(addScore(scores, " Alex ", 5));
System.out.println(addScore(scores, "Alex", -20));
System.out.println(getScore(scores, "Alex"));
scores.put("Alex", Integer.MAX_VALUE);
System.out.println(addScore(scores, "Alex", 1));
System.out.println(getScore(scores, "Alex"));
}
}运行结果
true
true
0
false
214748364709 / 14
排行榜先复制再排序
HashMap 适合按名字查分,不适合表达第1名、第2名。制作排行榜时,把记录复制到列表再排序。
- Map.entry复制不可修改值条目
- 按分数降序确定排名
- 同分按名字输出可重复
L26Example08.java · 完整独立示例
import java.util.*;
public class L26Example08 {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>(Map.of("Steve", 8, "Alex", 8));
List<Map.Entry<String, Integer>> ranking = new ArrayList<>();
scores.forEach((name, score) -> ranking.add(Map.entry(name, score)));
ranking.sort(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()));
scores.put("Alex", 99);
for (var entry : ranking)
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}运行结果
Alex=8
Steve=810 / 14
三类测试守住计分板
测试加分方法时,不只看一行输出。还要检查失败操作是否偷偷改变了 Map。
- 正常加分旧值增加
- 最低0分扣分夹紧
- 失败检查与操作前快照相等
| 场景 | 操作 | 预期 |
|---|---|---|
| 正常 | Alex加5 | 分数增加5 |
| 边界 | 现有0分玩家扣1 | 仍为0 |
| 错误 | 空名字加5 | 返回false 数据不变 |
| 错误 | null Map | 返回false 不抛异常 |
| 查询 | 不存在玩家 | 返回0 |
L26Example09.java · 完整独立示例
import java.util.*;
public class L26Example09 {
static boolean addScore(Map<String, Integer> scores,
String player, int delta) {
if (scores == null || player == null || player.isBlank()) return false;
String key = player.strip();
Integer old = scores.get(key);
if (scores.containsKey(key) && (old == null || old < 0)) return false;
int current = old == null ? 0 : old;
long next = (long) current + delta;
if (next > Integer.MAX_VALUE) return false;
scores.put(key, (int) Math.max(0L, next));
return true;
}
static int getScore(Map<String, Integer> scores, String player) {
if (scores == null || player == null || player.isBlank()) return 0;
String key = player.strip();
Integer value = scores.get(key);
if (!scores.containsKey(key)) return 0;
if (value == null || value < 0)
throw new IllegalStateException("计分记录损坏:" + key);
return value;
}
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>(Map.of("Alex", 12));
Map<String, Integer> before = new HashMap<>(scores);
boolean ok = addScore(scores, " ", 5);
System.out.println(!ok && scores.equals(before));
System.out.println(getScore(scores, "Mia"));
}
}运行结果
true
011 / 14
挑战 蓝图玩家计分板
挑战 程序叫 ScoreBoard。它要支持新玩家、老玩家加减分、安全查询和删除,同时不依赖 HashMap 的遍历顺序。
- 添加玩家不存在时从0起步
- 查询缺失本课返回0
- 删除玩家映射完整移除
验收顺序
- Alex 从12加5,查询得到17。
- Steve 从8扣20,最低分夹紧为0。
- 查询缺失 Mia 返回0,但 containsKey 可辨认缺失。
- 空名字操作返回 false,计分板不变。
- 删除 Steve 返回旧分数0,最后剩1名玩家。
12 / 14
计分板程序
新建 ScoreBoard.java,使用 Java 21 编译运行。
- Alex12加5得到17
- Steve8扣20得到0
- 删除 Steve最后剩1名玩家
ScoreBoard.java · 完整独立示例
import java.util.*;
public class ScoreBoard {
static boolean addScore(Map<String, Integer> scores,
String player, int delta) {
if (scores == null || player == null || player.isBlank()) return false;
String key = player.strip();
Integer old = scores.get(key);
if (scores.containsKey(key) && (old == null || old < 0)) return false;
int current = old == null ? 0 : old;
long next = (long) current + delta;
if (next > Integer.MAX_VALUE) return false;
scores.put(key, (int) Math.max(0L, next));
return true;
}
static int getScore(Map<String, Integer> scores, String player) {
if (scores == null || player == null || player.isBlank()) return 0;
String key = player.strip();
Integer value = scores.get(key);
if (!scores.containsKey(key)) return 0;
if (value == null || value < 0)
throw new IllegalStateException("计分记录损坏:" + key);
return value;
}
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
addScore(scores, "Alex", 12); addScore(scores, "Steve", 8);
addScore(scores, "Alex", 5); addScore(scores, "Steve", -20);
System.out.println("Alex=" + getScore(scores, "Alex"));
System.out.println("Steve=" + getScore(scores, "Steve"));
System.out.println("Mia=" + getScore(scores, "Mia"));
System.out.println("空名=" + addScore(scores, " ", 5));
System.out.println("删除=" + scores.remove("Steve"));
System.out.println("玩家数=" + scores.size());
}
}运行结果
Alex=17
Steve=0
Mia=0
空名=false
删除=0
玩家数=113 / 14
拓展任务与通关复盘
作业同时提交代码、测试表和一次故障记录。解释为什么选择某个字段当键。
- 选择键说明查找目的
- 定义失败缺失、空名、溢出
- 验证顺序不能依赖 HashMap 次序
三档作业
- 木镐:建立物品价格表,支持新增、改价和查询。
- 铁镐:统计一组掉落物的出现次数。
- 钻石镐:把计分记录复制到列表,输出分数最高的前三名。
| 自评 | 能 | 还要练 |
|---|---|---|
| 我会设计键和值 | □ | □ |
| 我能安全查询缺失键 | □ | □ |
| 我不依赖HashMap顺序 | □ | □ |
14 / 14
自己的验收记录
漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。
| 类别 | 你选择的操作 | 核对重点 |
|---|---|---|
| 正常 | 一个常用输入或一次成功动作 | 结果是否逐行一致 |
| 边界 | 范围端点与相邻值 | 有没有多一步、少一项 |
| 错误 | 非法数量、缺失对象或坏数据 | 是否明确拒绝,状态是否保持 |
- Bug 记录:触发条件、实际现象、原因、最小修复。
- 图示记录:为每条箭头说明起点、终点和含义。
- 改编记录:只改一个规则,同时修改预期和测试。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。