第一次到第三次大作业实验总结

tong1906 / 2025-01-20 / 原文


标题:第一次到第三次大作业实验总结


1. 引言

首先这三次作业中有明显的关联性,从第一次到第三次,作业难度也有明显的提升。在第一次作业中,只是简单的命令行测验系统,完成题目录入与解析,答题录入,题目排序与输出和自动判分的需求。在第二次作业中,在第一次的基础上增加了试卷验证,答卷完整性检查与补全等功能。而第三次作业对第一次增加了更多的功能和改动,如:学生信息处理、题目删除处理和题目格式与答卷的输入验证等。
例如:


2. 设计与分析

答题判题程序-1

1. 类设计

  • 1.1 Question 类
    • 职责:表示单个题目信息,包含题号、题目内容和正确答案。
    • 成员变量:
    • number:题号,用于排序和判题。
    • questionContent:题目内容。
    • correctAnswer:题目的正确答案。
    • 方法:
    • 构造方法:Question(int number, String questionContent, String correctAnswer) 初始化题目对象。
    • Getter 方法:获取题号、题目内容和正确答案的值。
  • 1.2 Quiz 类
    • 职责:管理题目列表、题目的排序与判题。
    • 成员变量:
    • questions:存储所有 Question 对象的列表。
    • 方法:
    • addQuestion:用于添加 Question 到题目列表。
    • getSortedQuestions:按题号对题目列表进行排序,并返回排序后的列表。
    • checkAnswers:接收用户的答题信息,逐题比对用户答案与正确答案,生成一个布尔列表,标识用户每题答对或答错。
  • 1.3 Main 类
    • 职责:作为程序入口,负责读取输入,输出结果,组织整个测试流程。
    • 主要流程:
    • 读取题目数量和内容,将题目存储在 Quiz 中。
    • 读取用户答案并调用判题逻辑输出答题结果。
    • 将题目和答案格式化输出。
      这里给出类图和生成报表:


2. 功能实现分析

  • 2.1 题目读取与存储
    • 主程序读取题目数量和具体内容,按以下格式解析题目输入:
    • 输入格式为 #N: <题号> #Q: <题目内容> #A: <正确答案>。
    • 通过 split() 方法按特殊字符分隔来获取题号、内容和正确答案。
    • 若输入格式不符合预期,程序将输出 "Invalid input format" 并终止。
  • 2.2 答案读取与判题
    • 主程序读取多行用户答题信息,直到输入 end 为止。
    • 答案格式假设为 <题号>:<用户答案>,例如 1:A,解析方式与题目相似。
    • 判题方法 checkAnswers 中,将用户答案与正确答案逐一比对,将判题结果存储为布尔值列表,表示每题是否正确。
  • 2.3 输出
    • 程序首先按顺序输出每题的内容和用户答案,格式为 <题目内容> ~ <用户答案>。
    • 随后,输出用户的判题结果,如: true false。

答题判题程序-2

1. 类设计

  • 1.1 Question 类
    • 职责:用于存储单个题目信息,包括题号、题目内容和正确答案。
    • 成员变量:
    • id:题目编号,标识题目。
    • query:题目内容。
    • answer:正确答案。
    • 方法:
    • 构造方法:Question(int id, String query, String answer) 初始化题目信息。
  • 1.2ExamPaper 类
    • 职责:用于表示试卷及其题目和对应分值的管理。
    • 成员变量:
    • paperId:试卷编号。
    • questions:题目编号到分值的映射(Map<Integer, Integer>),用于存储题目及其分数。
    • 方法:
    • 构造方法:ExamPaper(int paperId) 初始化试卷信息。
    • addQuestion:添加题目到试卷中,指定题号及分值。
    • getFullScore:计算试卷的满分(所有题目分值之和)。
  • 1.3 AnswerSheet 类
    • 职责:用于存储某一张答卷的考生答题信息。
    • 成员变量:
    • paperId:答卷对应的试卷编号。
    • answers:考生答题列表(List),按照试卷题目顺序保存考生答案。
    • 方法:
    • 构造方法:AnswerSheet(int paperId) 初始化答卷信息。
    • addAnswer:将考生答案添加到答题列表中。
  • 1.4 Main 类
    • 职责:作为程序入口,负责从控制台输入中解析题目信息、试卷信息和考生答题信息,并输出最终评分结果。
    • 主要流程:
    • 解析输入,分别处理题目、试卷和答卷信息。
    • 对每份答卷进行判分,并输出每题答题结果和总分。
      这里给出类图和生成报表:


2. 功能实现分析

  • 2.1 题目信息的解析与存储
    • 程序通过识别输入的前缀 #N: 来解析题目内容:
    • 从输入行解析题目编号、题目内容和正确答案,并存储在 questionMap 中。
    • 题目格式严格要求为 #N:<题号> #Q:<题目内容> #A:<正确答案>,如果不符合该格式,可能导致解析错误。
  • 2.2 试卷信息的解析与存储
    • 通过识别输入的前缀 #T: 来解析试卷信息:
    • 试卷包含一个编号,以及多个题目编号与分值的组合,如 #T:101 1-10 2-15,表示试卷编号为 101,题号 1 和 2 分别分值为 10 和 15。
    • 若试卷的总分不是 100,输出警告提示。
  • 2.3 答卷信息的解析与存储
    • 通过识别输入的前缀 #S: 来解析答卷信息:
    • 答卷包含一个试卷编号,以及按题目顺序排列的答案,如 #S:101 1:A 2:B 表示试卷编号为 101,题号 1 答案为 A,题号 2 答案为 B。
    • 答卷信息存储在 answerSheets 列表中。
  • 2.4 答题评分与结果输出
    • 对每份答卷执行以下评分流程:
    • 检查试卷是否存在,若不存在则输出错误提示。
    • 检查答卷是否包含每题答案,若缺少则补全为 "null" 并输出警告。
    • 对每题判分,若考生答案正确则计入对应分值,否则记 0 分。
    • 输出每题的题目内容、考生答案、对错情况。
    • 输出每题得分及总得分。

答题判题程序-3

1. 类设计

  • 1.1 Question 类
    • 职责:用于存储单个题目信息,包括题号、题目内容和正确答案。
    • 成员变量:
    • id:题目编号,唯一标识题目。
    • query:题目内容,用于描述问题。
    • answer:正确答案,用于判分。
    • 方法:
    • 构造方法:Question(int id, String query, String answer) 初始化题目信息,包括题号、内容和正确答案。
  • 1.2ExamPaper 类
    • 职责:用于表示试卷及其题目和对应分值的管理。
    • 成员变量:
    • paperId:试卷编号。
    • questions:题目编号到分值的映射(Map<Integer, Integer>),用于存储题目及其分数。
    • 方法:
    • 构造方法:ExamPaper(int paperId) 初始化试卷信息。
    • addQuestion:添加题目到试卷中,指定题号及分值。
    • getFullScore:计算试卷的满分(所有题目分值之和)。
  • 1.3 AnswerSheet 类
    • 职责:用于存储某一张答卷的考生答题信息。
    • 成员变量:
    • paperId:答卷对应的试卷编号。
    • answers:考生答题列表(List),按照试卷题目顺序保存考生答案。
    • 方法:
    • 构造方法:AnswerSheet(int paperId) 初始化答卷信息。
    • addAnswer:将考生答案添加到答题列表中。
  • 1.4 Student 类
    • 职责:用于存储学生信息,包括学生编号和姓名。
    • 成员变量:
    • studentId:学生编号,用于唯一标识每个学生。
    • studentName:学生姓名。
    • 方法:
    • 构造方法:Student(String studentId, String studentName) 初始化学生信息。
  • 1.5 Main 类
    • 职责:作为程序入口,负责从控制台输入中解析题目信息、试卷信息、考生信息和考生答题信息,并输出最终评分结果。
    • 主要流程:
    • 解析输入,分别处理题目、试卷和答卷信息。
    • 对每份答卷进行判分,并输出每题答题结果和总分。
      这里给出类图和生成报表:


2. 功能实现分析

  • 2.1 题目信息的解析与存储
    • 程序通过识别输入的前缀 #N: 来解析题目内容:
    • 每行格式应为 #N:<题号> #Q:<题目内容> #A:<正确答案>,包含题号、题目内容和正确答案。
    • 若格式不符合要求,系统记录并输出警告信息。
    • 题目信息存储在 questionMap 中,键为题目编号,值为 Question 对象。
  • 2.2 试卷信息的解析与存储
    • 通过识别输入的前缀 #T: 来解析试卷信息:
    • 试卷包含一个编号及多个题目-分值组合,例如 #T:101 1-10 2-15 表示试卷编号为101,题号1的分值为10,题号2的分值为15。
    • 系统在 examPaperMap 中存储试卷信息,若试卷总分不是100,系统记录并输出警告信息。
  • 2.3 学生信息的解析与存储
    • 通过识别输入的前缀 #X: 来解析学生信息:
    • 每行包含一个或多个学生编号和姓名的组合,格式如 #X: 1001 张三 1002 李四。
    • 学生信息存储在 studentMap 中,键为学生编号,值为 Student 对象。
  • 2.4 答卷信息的解析与存储
    • 通过识别输入的前缀 #S: 来解析答卷信息:
    • 每行包含试卷编号、考生编号及按题目顺序排列的答案,例如 #S:101 1001 #A:1-A #A:2-B 表示试卷编号101、学生编号1001的答案为题号1答A、题号2答B。
    • 若答卷缺少必要信息(试卷编号或学生编号),系统记录并输出警告。
    • 答卷信息存储在 answerSheets 列表中,每个元素为 AnswerSheet 对象。
  • 2.5 题目删除信息的解析与存储
    • 通过识别输入的前缀 #D: 来解析删除题目信息:
    • 格式为 #D: N-1 N-2 表示删除题号1和题号2的题目。
    • 被删除的题目ID存储在 deletedQuestions 集合中,以便在判卷时过滤。
      2.6 答题评分与结果输出
    • 系统对每份答卷执行以下评分流程:
    • 检查试卷是否存在,若不存在则输出错误提示。
    • 检查答卷是否包含每题答案,若缺少则记为"null"并输出警告。
    • 按试卷中的题目顺序逐题判分,若考生答案正确则计入对应分值,否则记为0分。
    • 输出每题的题目内容、考生答案、对错情况。
    • 输出每题得分及总得分,将最终结果按格式化字符串输出。。

3. 采坑心得

3.1 :作业一

格式验证不足

用户输入的格式可能不完全符合预期格式。例如,题目内容的格式要求严格:#N:题号 #Q:题目内容 #A:答案,任何格式不符合都会导致 ArrayIndexOutOfBoundsException。
解决方法:提前验证输入格式,确保 #N:, #Q:, #A: 都存在,避免出现格式不匹配的情况。可以在 addQuestion 前先验证每个分割的部分是否存在并符合预期,提示用户修正输入格式。

// 读取题目内容,保留原格式
for (int i = 0; i < questionCount; i++) {
    String input = scanner.nextLine();

    // 验证是否包含必需的标识符
    if (!input.contains("#N:") || !input.contains("#Q:") || !input.contains("#A:")) {
        System.out.println("Invalid input format. Expected format: #N:<number> #Q:<question> #A:<answer>");
        return;
    }

    String[] numberPart = input.split("#N:");
    if (numberPart.length > 1 && !numberPart[1].isBlank()) {
        try {
            int number = Integer.parseInt(numberPart[1].split(" ")[0].trim());
            String questionContent = input.split("#Q:")[1].split(" #A:")[0];
            String correctAnswer = input.split("#A:")[1].trim();
            quiz.addQuestion(new Question(number, questionContent, correctAnswer));
        } catch (Exception e) {
            System.out.println("Error parsing question input. Please check the format.");
            return;
        }
    } else {
        System.out.println("Invalid input format.");
        return;
    }
}

提示:在 Main 类中读取题目内容时,加入对 #N:, #Q:, #A: 格式的验证,确保符合预期格式,之后在用户答案格式验证也是同样。格式的验证同样是我们之后作业的基础。


3.2 :作业二

答卷解析和缺失答案的处理

在答卷解析时,答卷可能缺少某些题目的答案。在代码中,添加了将缺失答案补全为 "null" 的逻辑,但此时需要额外判断答案是否存在,避免空答案导致评分错误。
解决方法:在补全答案之前,输出警告信息并明确说明补全的内容。另外,可以在 addAnswer 方法中加入对空答案的检查逻辑,减少重复判断。

// 解析答卷信息
int paperId = Integer.parseInt(line.split(" ")[0].substring(3));
AnswerSheet answerSheet = new AnswerSheet(paperId);
String[] answers = line.split(" ");
for (int i = 1; i < answers.length; i++) {
    answerSheet.addAnswer(answers[i].substring(3));
}
answerSheets.add(answerSheet);  // 添加每一份答卷

提示:这部分是我在解析答卷信息的的主要逻辑,用于将每一份答卷转换为可处理的 AnswerSheet 对象,并将其存储在 answerSheets 列表中。

// 检查答卷中的答案是否足够,如果不足则补全,并发出警告
if (sheet.answers.size() < paper.questions.size()) {
    System.out.println("Warning: The answer sheet for paper " + sheet.paperId + " is incomplete. Missing " + 
                        (paper.questions.size() - sheet.answers.size()) + " answers.");
    
    // 补全缺失的答案为 "null"
    while (sheet.answers.size() < paper.questions.size()) {
        sheet.answers.add("null");
    }
}

提示:这部分用于缺失答案的检查和补全逻辑在主程序循环中输出答卷结果。

数据结构的选用

问题:代码中使用了 HashMap 来存储 questionMap 和 examPaperMap,但 HashMap 默认无序,导致输出顺序可能不符合输入顺序,特别是在需要保持题目顺序的情况下。
解决方法:可以将 HashMap 换成 LinkedHashMap,确保按照输入顺序进行输出。在 ExamPaper 类的 questions 字段中,已使用 LinkedHashMap 保证题目顺序一致,这个结构选择很适合题目排序的需求。

Map<Integer, Question> questionMap = new HashMap<>();  // 题目编号与题目信息的映射
Map<Integer, Question> questionMap = new LinkedHashMap<>();  // 题目编号与题目信息的映射(有序)
Map<Integer, ExamPaper> examPaperMap = new HashMap<>(); // 试卷编号与试卷信息的映射
Map<Integer, ExamPaper> examPaperMap = new LinkedHashMap<>(); // 试卷编号与试卷信息的映射(有序)

提示:在 questionMap 和 examPaperMap 中使用 LinkedHashMap 可以确保题目和试卷信息按输入顺序输出。


3.3 :作业三

题目 ID 重复问题

代码会将题目 ID 和题目信息存储在 questionMap 中,而没有检查题目 ID 是否已经存在。如果题目 ID 重复,后面的题目将覆盖前面的内容。解决方法是:
在插入题目前检查:在 questionMap.put() 之前,检查是否已经包含该 questionId,如果已存在则加入 alerts 提示重复题目,或决定跳过或覆盖。

// 处理题目信息
if (line.startsWith("#N:")) {
    String[] parts = line.split(" ");
    try {
        int questionId = Integer.parseInt(parts[0].substring(3));
        
        // 检查题目 ID 是否重复
        if (questionMap.containsKey(questionId)) {
            alerts.add("alert: Duplicate question ID " + questionId + " in line: " + line);
            continue; // 跳过当前题目,避免覆盖
        }
        
        if (parts.length < 3 || !parts[1].startsWith("#Q:") || !parts[2].startsWith("#A:")) {
            throw new IllegalArgumentException("wrong format: " + line);
        }
        
        String query = parts[1].substring(3);
        String answer = parts[2].substring(3);
        questionMap.put(questionId, new Question(questionId, query, answer));
        
    } catch (Exception e) {
        alerts.add("wrong format: " + line);
    }
}。

提示:在初始代码中没有专门针对题目 ID 重复的处理逻辑,之后在questionMap 之前添加检查逻辑,判断题目 ID 是否已经存在,并在发现重复时记录一条警告信息,虽然不太符合pta的作业需求,算是课后的一点补充。


4. 改进建议

第一次作业

  • 异常处理:当输入格式不符合预期时可改进异常处理,而不仅仅输出"Invalid input format"。
  • 输入校验:可以增加对用户输入的校验,确保输入的题目和答案格式严格符合预期。
  • 题目和答案之间的关系:当前Quiz类仅判断答案是否完全相同,可以通过正则或其他算法实现模糊匹配

第二次作业

  • 答卷判分逻辑改进:目前判题为严格匹配,可以引入模糊匹配或字符串忽略大小写.
  • 优化输出:可考虑将输出集中到一个方法中,提高代码的可读性和复用性。

第三次作业

  • 输入解析的封装
    目前输入解析是直接在main方法中实现的,导致main方法过长且复杂。可以将不同指令的解析(如#N:, #T:, #X:, #S:, #D:)拆分成独立的方法,例如parseQuestion、parseExamPaper
    等方法。这样可以提高代码的清晰度和维护性

5. 总结

在第一次作业中,我通过面向对象的方式,创建了一个灵活的题库与判题系统,并利用字符串处理实现了丰富的输入输出格式,较好地满足了题目管理与多组用户判题的需求。
在第二次作业中,实现了题库管理、试卷管理和答卷评分的基本功能,并通过设计各类结构实现了代码的模块化和清晰化,使得题目、试卷和答卷的管理更为简洁且高效。
在第三次作业中,实现了一个较完整的考试系统,涵盖了题目管理、试卷生成、学生信息、答题评分和错误提示等功能。