文章目录
前言
在开发在线考试或问答系统时,经常会遇到一个痛点:考生容易通过背诵选项字母(例如"第一题选C,第二题选A")来作弊或应付考试。
为了解决这个问题,我们需要在系统渲染试卷时,将题目的选项顺序随机打乱。
这里的核心难点在于:选项顺序打乱后,系统必须依然能准确识别出哪一个是正确答案。
这就好比给房间里的物品换门牌号:物品本身没动,只是门上的字母标签换了,但我们需要时刻追踪那个存放"正确答案"的房间现在挂着什么字母。
一、 核心思路
| 原标签 (originalKey) | 新标签 (newKey) | 选项内容 (value) | 是否为正确答案 |
|---|---|---|---|
| A | C | JAVA2 EE | 否 |
| B | A | JAVA2 Card | 否 |
| C | D | JAVA2 ME | 否 |
| D | B | JAVA2 HE | ✓ → 触发记录 keyNew = "B" |
内容没有动,只是换了门牌号(字母标签)。正确答案的内容跟着走到了 B,所以正确的选项 Key 也同步更新为 B。
二、核心代码实现
为了实现这个功能,我们首先需要一个数据结构来承载选项和答案。
2.1 定义数据承载类 Topic
这个类非常简单,主要用于封装选项的 Map(选项字母 -> 选项内容)和正确的 key。
Java
import java.util.Map;
public class Topic {
private Map<String, String> option; // 选项映射,例如 {"A": "JAVA2 EE", ...}
private String key; // 正确答案的选项字母
public Topic() {}
public Topic(Map<String, String> option, String key) {
this.option = option;
this.key = key;
}
public Map<String, String> getOption() { return option; }
public void setOption(Map<String, String> option) { this.option = option; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
}
2.2 乱序工具类 TopicRandomUtil
这是整个乱序逻辑的引擎。利用 Collections.shuffle() 来实现"摇匀门牌号"的操作。
在 Java 中,HashMap 的 keySet() 是无序的,它既不保证插入顺序,也不保证字母顺序。它仅仅是根据 Key 的哈希值(Hash Code)计算出的数组下标来存放的。
Java
import java.util.*;
public class TopicRandomUtil {
/**
* 乱序Map元素,记录对应答案key
* @param option 原题目选项
* @param key 原正确答案
* @return Topic 乱序后的选项和新的正确答案
*/
static public Topic random(Map<String, String> option, String key) {
// 1. 获取原有的门牌号集合 (A, B, C, D)
Set<String> keySet = option.keySet();
ArrayList<String> keyList = new ArrayList<>(keySet);
// 2. 将门牌号列表乱序 (例如变成了 C, A, D, B)
Collections.shuffle(keyList);
// 3. 准备一个新的Map来存放换好门牌号的选项
HashMap<String, String> optionNew = new HashMap<>();
int idx = 0;
String answerNew = ""; // 用于记录新的正确答案门牌号
// 4. 遍历原有的门牌号,依次贴上新的门牌号
for (String originKey : keySet) {
String newKey = keyList.get(idx++);
// 5. 判断当前被替换门牌号的原选项,是不是正确答案
if (key.equals(originKey)) {
answerNew = newKey; // 如果是,记录它贴上的新门牌号
}
// 6. 将新门牌号和原内容存入新Map
optionNew.put(newKey, option.get(originKey));
}
return new Topic(optionNew, answerNew);
}
}
算法可视化流程:

3. 实际业务场景模拟验证
为了直观看到效果,我们编写一个简单的 main 方法来模拟这个过程:
Java
import java.util.HashMap;
import java.util.Map;
public class ExamSimulation {
public static void main(String[] args) {
// 1. 模拟从数据库中读取了一道题
Map<String, String> originalOptions = new HashMap<>();
originalOptions.put("A", "JAVA2 EE");
originalOptions.put("B", "JAVA2 Card");
originalOptions.put("C", "JAVA2 ME");
originalOptions.put("D", "JAVA2 HE");
String originalKey = "D"; // 原正确答案是 D: JAVA2 HE
System.out.println("====== 打乱前 ======");
System.out.println("题目选项: " + originalOptions);
System.out.println("正确答案: " + originalKey + " -> " + originalOptions.get(originalKey));
// 2. 使用乱序器打乱选项
Topic randomizedTopic = TopicRandomUtil.random(originalOptions, originalKey);
// 3. 打印打乱后的结果
System.out.println("\n====== 打乱后 ======");
System.out.println("题目选项: " + randomizedTopic.getOption());
System.out.println("正确答案: " + randomizedTopic.getKey() + " -> " + randomizedTopic.getOption().get(randomizedTopic.getKey()));
// 验证:内容是否依然对应
System.out.println("\n验证结果:答案内容是否改变? " +
(!originalOptions.get(originalKey).equals(randomizedTopic.getOption().get(randomizedTopic.getKey())) ? "改变了(错误)" : "没改变(正确)"));
}
}
模拟运行输出结果示例:
====== 打乱前 ======
题目选项: {A=JAVA2 EE, B=JAVA2 Card, C=JAVA2 ME, D=JAVA2 HE}
正确答案: D -> JAVA2 HE
====== 打乱后 ======
题目选项: {A=JAVA2 EE, B=JAVA2 ME, C=JAVA2 HE, D=JAVA2 Card}
正确答案: C -> JAVA2 HE
验证结果:答案内容是否改变? 没改变(正确)
总结
通过分离"标签(Key)"和"内容(Value)",利用 Collections.shuffle() 打乱标签集合,并在重新绑定的过程中通过内容比对找回正确答案的新标签。