← Java 基础教程

第 26 / 40 课 · 免费教程

计分板与 HashMap

按名字查分,安全更新计分板

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

本课作品

制作玩家计分板 ScoreBoard

服务器里来了新玩家,计分板无需再添一个变量。以名字为键,读出分数,再用明确规则处理缺失、扣分、排序与整数上限。 前置背包:变量、方法、循环、ArrayList 和泛型。今天让第100位玩家加入时也不必创建 player100Score。

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

玩家名字连接自己的分数
  1. key玩家名,唯一查找标识
  2. value分数,可重复
  3. entry一条键值映射

01 / 14

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

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

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

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

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

02 / 14

键和值组成查找表

Map 像服务器计分板。玩家名字是键,分数是值;程序通过名字快速找到对应分数。

同一个键只保留一份当前值。两个不同玩家可以恰好拥有相同分数。

玩家名字连接自己的分数
  1. key玩家名,唯一查找标识
  2. value分数,可重复
  3. entry一条键值映射
验收对照表
概念计分板例子规则
键 key玩家名 Alex在同一 Map 中唯一
值 value分数 12允许重复
映射 entryAlex 对应 12一组键和值

03 / 14

创建 HashMap 并放入数据

声明时写出键和值的类型。尖括号中的 String, Integer 表示只能使用字符串键和整数值。

put 添加或替换 get 按键查询
  1. Map<String,Integer>明确键值类型
  2. put同名键替换旧值
  3. 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
2

BadMapType.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 和 merge 都能处理新玩家
  1. 不存在getOrDefault 给默认0
  2. 已存在 null默认值不会代替它
  3. 安全加法大数先转 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"));
    }
}

运行结果

8

05 / 14

不存在的键要明确处理

get 对不存在的键返回 null。计分板可以选择显示0,也可以明确告诉用户找不到玩家。

containsKey 区分没有玩家和分数为0
  1. get缺失键返回 null
  2. containsKey判断键是否存在
  3. 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
false

06 / 14

entrySet 一次拿到键和值

只遍历 keySet 再调用 get 也能工作,但 entrySet 直接提供完整记录,表达更清楚。

遍历每条计分记录
  1. entrySet同时拿到键与值
  2. HashMap不承诺遍历顺序
  3. 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=8

07 / 14

删除玩家并理解顺序

remove(key) 删除整条记录,并返回旧值;键不存在时返回 null。clear() 会清空整张表。

remove 返回旧值 HashMap 不排队
  1. remove(key)删除并返回旧值
  2. clear()清空全部记录
  3. 展示顺序需明确选择实现

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
0

08 / 14

方法参数面向 Map 合同

方法若只需要 Map 的通用操作,参数应写 Map<K,V>,而不是锁死为 HashMap。

调用者可选择不同 Map 实现
  1. 名字检查拒绝空名,去首尾空白
  2. long 计算防止 int 相加溢出
  3. 更新最后拒绝时数据不变

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
2147483647

09 / 14

排行榜先复制再排序

HashMap 适合按名字查分,不适合表达第1名、第2名。制作排行榜时,把记录复制到列表再排序。

Map 负责查找 List 负责有序展示
  1. Map.entry复制不可修改值条目
  2. 按分数降序确定排名
  3. 同分按名字输出可重复

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=8

10 / 14

三类测试守住计分板

测试加分方法时,不只看一行输出。还要检查失败操作是否偷偷改变了 Map。

正常 边界 错误输入都要留下证据
  1. 正常加分旧值增加
  2. 最低0分扣分夹紧
  3. 失败检查与操作前快照相等
验收对照表
场景操作预期
正常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
0

11 / 14

挑战 蓝图玩家计分板

挑战 程序叫 ScoreBoard。它要支持新玩家、老玩家加减分、安全查询和删除,同时不依赖 HashMap 的遍历顺序。

添加 扣分 查询 删除形成完整流程
  1. 添加玩家不存在时从0起步
  2. 查询缺失本课返回0
  3. 删除玩家映射完整移除

验收顺序

  • Alex 从12加5,查询得到17。
  • Steve 从8扣20,最低分夹紧为0。
  • 查询缺失 Mia 返回0,但 containsKey 可辨认缺失。
  • 空名字操作返回 false,计分板不变。
  • 删除 Steve 返回旧分数0,最后剩1名玩家。

12 / 14

计分板程序

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

Map 合同封装安全的计分操作
  1. Alex12加5得到17
  2. Steve8扣20得到0
  3. 删除 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
玩家数=1

13 / 14

拓展任务与通关复盘

作业同时提交代码、测试表和一次故障记录。解释为什么选择某个字段当键。

三档任务扩建服务器统计
  1. 选择键说明查找目的
  2. 定义失败缺失、空名、溢出
  3. 验证顺序不能依赖 HashMap 次序

三档作业

  • 木镐:建立物品价格表,支持新增、改价和查询。
  • 铁镐:统计一组掉落物的出现次数。
  • 钻石镐:把计分记录复制到列表,输出分数最高的前三名。
验收对照表
自评能还要练
我会设计键和值□□
我能安全查询缺失键□□
我不依赖HashMap顺序□□

14 / 14

自己的验收记录

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

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

TAKE IT WITH YOU

下载本课 PDF

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