一、前言
本文介绍了如何使用Java设计实现答题程序,模拟一个小型的测试,要求输入题目信息和答题信息,根据输入题目信息中的标准答案判断答题的结果。本次编程任务通过三次作业实现了从基本的题目管理到完整的测试系统的设计。
第一次作业主要实现了题目信息、试卷信息和答卷信息的基本处理,包括输入解析、数据存储和简单的判题逻辑。
第二次作业在此基础上增加了学生信息的处理,要求将学生信息与答卷信息关联,并在校验和输出时带上学生的相关信息。
第三次作业进一步引入了题目删除的功能,要求处理题目删除后对题目显示、答案判断及得分计算的影响,并处理各种异常情况,如题目引用错误和格式错误等。
随着任务的进展,难度逐步增加,从基本的数据结构设计和逻辑处理,到更复杂的关联数据管理、异常处理及输出格式控制。这一系列任务不仅涵盖了输入输出的基本技能,还要求具备良好的数据管理和逻辑设计能力,最终形成一个功能完备的小型测试系统。
二、设计与分析
建立框架:
- 类定义
(1)Question 类:
用于表示一个题目,包含题目编号 (number)、题目内容 (content) 和标准答案 (standardAnswer)。
提供了一个 judgeAnswer 方法来判断答案是否正确。
(2)Exam 类:
用于表示一套题目集合,包含一个题目数组 (questions) 和题目数量 (count)。
提供方法 saveQuestion 来保存题目。
提供方法 getQuestions 返回排序后的题目数组。
提供方法 getCount 获取题目数量。
(3)Paper 类:
用于表示一份答卷,包含一个考试对象 (exam)、答案数组 (answers) 和判断结果数组 (judgments)。
提供方法 saveAnswer 来保存答案。
提供方法 judgeAnswers 来判断答案是否正确。
提供方法 displayAnswersAndJudgments 来显示题目内容、答案和判断结果。
提供方法 outputJudgments 来输出判断结果。
(4)设计类图如下:
-
主程序逻辑
主函数 (main 方法):
初始化 Scanner 对象用于读取输入。
读取题目数量,并创建一个 Exam 对象。
逐行读取题目信息,并创建 Question 对象,保存到 Exam 中。
读取答题信息,并创建一个 Paper 对象。
解析答题信息,并保存到 Paper 中。
调用 judgeAnswers 方法进行答案判断。
显示答题信息和判断结果。 -
代码实现细节
(1)输入处理:
使用 Scanner 类来读取输入。
读取题目数量,并根据数量初始化 Exam 对象。
逐行读取题目信息,使用正则表达式或字符串分割来解析题目编号、题目内容和标准答案。
创建 Question 对象,并保存到 Exam 对象中。
(2)题目保存:
Exam 类的 saveQuestion 方法用于保存题目到数组中,根据题目编号进行索引。
(3)排序题目:
Exam 类的 getQuestions 方法返回排序后的题目数组,确保题目按照编号顺序排列。
(4)答案保存与判断:
Paper 类的 saveAnswer 方法用于保存答案到数组中。
Paper 类的 judgeAnswers 方法遍历题目数组,并调用 Question 类的 judgeAnswer 方法来判断答案是否正确。
(5)输出结果:
Paper 类的 displayAnswersAndJudgments 方法用于输出题目内容、答案和判断结果。
Paper 类的 outputJudgments 方法用于输出判断结果。
第一次作业:
输入:题目数量、题目内容(包括题号、题目内容、标准答案)、答题信息。
输出:题目数量、答题信息(题目内容 + 答案)、判题信息(判断每个答案是否正确)。
要求:程序需要处理输入的题目信息和答题信息,并根据标准答案判断答题结果。题目和答案的顺序可以不同,但答案的顺序必须与题号对应。
第二次作业:
在第一次作业上修改部分:
1.处理试卷信息:
代码中定义了 TestPaper 类来存储试卷信息,包括试卷号和每道题的分值。
在主函数中,通过读取输入行并解析,将试卷信息存储到 HashMap<String, TestPaper> 中。
2.处理总分警示:
在主函数中,遍历 HashMap 中的每份试卷,检查总分是否为100分,并将警告信息存储到 List
3.处理答案数量不足的情况:
在处理答卷时,如果输入的答案信息少于试卷的题目数量,没有答案信息的题目输出 "answer is null"。
多余的答案信息在代码中被忽略,即只处理有效范围内的答案。
4.输出判分信息:
输出格式包含每道题的得分以及总分。
使用 StringBuilder 来构造输出字符串,确保格式正确。
5.提示错误的试卷号:
如果输入的答案信息中的试卷号不存在,则输出提示信息 "The test paper number does not exist"。
第三次作业:
增补或修改的部分:
- 学生信息处理
在代码中,students 变量是一个 HashMap<String, String>,用来存储学生的学号和姓名。这是通过以下方式实现的:
else if (line.startsWith("#X:")) {
String[] parts = line.split(" ");
String studentId = parts[0].split(":")[1].trim();
String studentName = parts[1].split("-")[0].trim();
students.put(studentId, studentName);
}
这里,#X: 开头的行被解析成学号和姓名,并存储在 students 映射中。
-
删除题目信息处理
在代码中,deletedQuestions 是一个 HashSet,用来存储被删除的题号。这是通过以下方式实现的:
else if (line.startsWith("#D:")) {
String parts = line.split("-")[1].trim();
deletedQuestions.add(parts);
}
这里,#D: 开头的行被解析成题号,并添加到 deletedQuestions 集合中。 -
试卷号引用错误提示输出
在处理 #S: 开头的行时,会检查试卷号是否存在:
if (!testPapers.containsKey(paperId)) {
resultsToOutput.add("The test paper number does not exist");
continue;
}
如果不存在,则输出 "The test paper number does not exist" 并继续处理下一行。
- 学号引用错误提示信息
在处理每份答卷时,会检查学号是否存在:
String studentName = students.get(studentId);
if (studentName == null) {
resultsToOutput.add(studentId + " not found");
} else {
resultsToOutput.add(studentId + " " + studentName + ": " + finalScores + "~" + totalScore);
}
如果学号不存在,则输出 "studentId not found",否则输出学号、姓名、得分和总分。
- 题目引用错误提示信息
在处理每份答卷时,会检查题目是否存在:
Question question = questions.stream()
.filter(q -> q.getNumber().equals(questionNumber))
.findFirst()
.orElse(null);
if (question == null) {
questionResults.add("non-existent question~0");
scoresReport.append("0 ");
continue;
}
如果题目不存在,则输出 "non-existent question~0"。
- 输出部分
最后,所有的输出信息都被收集到 resultsToOutput 列表中,并逐一打印出来:
for (String output : resultsToOutput) {
System.out.println(output);
}
三、问题解决
1.错误提示的优先级:
第三次改动后,代码格式中加入了很多错误输入及其提示信息,当一个输入中同时发生两个及以上的错误,我们必须考虑优先显示哪一类错误提示。
为了实现错误提示的优先级处理,我们需要在代码中根据优先级顺序来捕获和处理异常。首先,我们需要定义一系列特定的异常类来区分不同类型的错误。在处理每一行输入时,我们应该按照优先级顺序来捕获和处理异常:
(1)格式错误:首先处理输入格式错误。
(2)答案没有输入:其次处理答案没有输入的情况。
(3)题目编号唯一性验证:处理题目编号是否唯一。
(4)无效题目引用:处理题目编号在试卷中不存在的情况。
(5)学生学号唯一性验证:处理学生学号是否唯一。
2.学生信息的存储:
第三次改动后,需要录入学生信息并且查询它,我一开始使用动态数组进行存储,但发现数组无法直接保证数据的唯一性。如果试图存储具有唯一标识符(如学号)的学生信息,而这些标识符在数组中重复出现,那么就需要额外的逻辑来检查重复项,这增加了代码的复杂性。
替代方案:使用映射表
import java.util.HashMap;
public class Main {
private static Map<String, String> students = new HashMap<>();
public static void main(String[] args) {
// 添加学生信息
students.put("12345", "张三");
students.put("67890", "李四");
// 查找学生信息
String searchId = "12345";
String foundName = students.get(searchId);
if (foundName != null) {
System.out.println("找到了学生: " + foundName);
} else {
System.out.println("未找到学生");
}
}
}
四、改进建议
1.模块化设计:
目前所有的逻辑都在main方法中处理,可以考虑将其拆分成更小的方法或者独立的类来提高可维护性和可测试性,比如:
(1)创建一个类来专门处理输入数据的解析。在这个类中,你可以定义多个静态方法,每个方法负责解析一种类型的输入数据。
(2)我们还可以创建一个类来负责验证输入数据的正确性。在这个类中,你可以定义多个静态方法,每个方法负责校验一个方面的数据。
2.异常处理
当前的程序中,异常处理部分比较宽泛,仅通过一个简单的catch块来捕获所有异常,并简单地记录错误信息。为了提高异常处理的精确度和可读性,可以根据具体的业务逻辑,可以定义一些特定的异常类来表示不同的错误情况。当输入格式不符合预期、试卷ID无效、题目ID无效和学生ID无效时抛出不同的异常类。然后,在处理输入时,针对不同的错误情况抛出不同的异常,并在catch块中分别处理。
3.输入验证:
输入验证是确保程序正确处理数据的关键步骤之一。通过增加更多的输入验证逻辑,可以提前发现潜在的问题,确保输入数据的有效性:
(1)题目编号唯一性验证:可以使用一个Set来存储已有的题目编号,每次添加新题目时检查是否已经存在。
(2)试卷题目分数组合验证:解析试卷时,需要确保题目分数组配合理,并且总分为100分。同时,还需要验证题目编号是否与题目列表中的编号匹配。
(3)学生学号唯一性验证:在解析学生信息时,需要确保学号唯一。可以使用一个Set来存储已有的学号,每次添加新学生时检查是否已经存在。
(4)答案数量一致性验证:在解析答案时,需要确保答案数量与试卷中的题目数量一致。如果不一致,则记录错误信息。
五、总结
通过本次三次作业的实践,我学到了以下几个方面:
1.数据结构的应用:
学会了使用映射表(如 HashMap)来存储和查询数据,特别是处理具有唯一标识符的数据时,映射表的优势明显。
使用集合(如 HashSet)来存储唯一数据,如题目编号和学生学号,确保数据的唯一性。
2.面向对象编程:
通过定义 TestPaper、Question、AnswerSheet 等类,掌握了面向对象的设计思想,增强了代码的可读性和可维护性。
定义了多个异常类来处理不同的错误情况,提高了程序的健壮性和异常处理的能力。
3.输入输出处理:
学会了如何解析不同格式的输入数据,并对其进行有效性验证。
使用 StringBuilder 等工具来格式化输出结果,确保输出的规范性和清晰度。
4.模块化设计:
通过将不同的功能封装成独立的方法或类,提高了代码的模块化程度,便于后续的维护和扩展。
尽管通过本次作业学到了很多,但在实际应用中还有许多需要进一步学习和研究的地方:
1.异常处理的精细化:
当前的异常处理相对粗略,可以通过定义更多具体的异常类来细化异常处理逻辑,从而更好地反映程序状态。
2.输入验证的全面性:
当前的输入验证主要集中在基本的格式和唯一性上,可以进一步增加更复杂的验证逻辑,如数据范围、格式正确性等。
3.性能优化:
在数据量较大的情况下,可以进一步优化算法和数据结构,提高程序的运行效率。
4.用户交互:
可以增加用户交互界面,使用户能够更直观地查看输入输出结果,提高用户体验。