前三次大作业总结
一、前言
1. 题目一
1.1 知识点
1.1.1 类设计和封装:
- 题目类:设计题号、题目内容和标准答案的基本属性,包含获取和设置方法,支持答案比对方法,用于判断答题是否正确。
- 试卷类:设计题目列表和题目数量两个属性,包含将题目存储到列表中的方法,能够按题号排序,以便在输出时确保题目顺序。
- 答卷类:设计包含答案列表和判题结果列表的基本结构,具备保存答案、判断答案正确性、输出答题结果的功能。
1.1.2 输入解析:
- 按行解析题目数量、题目信息和答题信息,进行格式化存储。
- 实现题目顺序无关的解析方法,确保输入可以无序排列。
- 解析包含分隔符的答案信息,能够将不同题目的答案存储并对应判题。
1.1.3 判题逻辑:
- 判断每个答案是否符合题目的标准答案,输出正确与否。
- 输出时确保答题结果按题号顺序排序,并将题目内容和判题结果整合输出。
1.2 题量
- 1.2.1 基本题量:实现题目类、试卷类和答卷类三大类的基本功能。
- 1.2.2 实现内容:实现题目输入、题号无序解析、答题信息的比对和结果输出。
1.3 难度
- 比较难:需要掌握面向对象设计和类的封装,对应答题的基本输入和判题输出进行处理。
2. 题目二
2.1 知识点
2.1.1 扩展类设计:
- 题目类:保持不变,仍包含题号、题目内容和标准答案。
- 试卷类:扩展包含多个试卷的存储功能,支持每张试卷题号和分值的存储。
- 答卷类:扩展支持多张答卷的存储,具备按试卷顺序解析答案并计算分数的能力。
2.1.2 输入解析增强:
- 无序输入解析:题目信息、试卷信息和答题信息可以混合输入,并且不按照顺序给出。需要设计多遍输入解析流程,以确保先解析题目信息,后解析试卷信息,最后解析答题信息。
- 分值验证:根据试卷中的分值累加判断是否满分100,不满足时给出警告。
2.1.3 判题输出增强:
- 题目输出中加入答题是否正确的结果(True/False),对于答案为空的题目显示“answer is null”。
- 分数计算:根据判题结果,统计每道题目的得分,并累加为总分输出。
2.1.4 错误处理:
- 无效试卷号:答卷信息中出现的无效试卷号需单独提示,输出错误信息。
- 缺失答案处理:答案数量小于题目数量时,缺失题目计0分。
2.2 题量
- 增加题量:相比第一题,题目量增加至多张试卷和多份答卷,且需要在试卷类中增加分值和试卷总分的校验。
- 复杂实现:实现多张试卷、多份答卷的输入解析,包含分值判分逻辑的增强。
2.3 难度
- 很难:在理解面向对象设计的基础上,需要掌握多类信息解析和处理,尤其是解析无序输入的能力,并扩展判题和分数计算的功能。
3. 题目三
3.1 知识点
3.1.1 进一步扩展类设计:
- 学生类:增加了学生信息的存储,包含学号和姓名,通过学号关联答卷信息。
- 删除信息类:包含题号的删除功能,在题目被删除时,试卷引用的该题目判0分,并提示“the question invalid”。
- 题目引用错误处理:设计无效题目引用检测机制,对于引用无效题目或试卷信息的情况进行单独提示。
3.1.2 复杂数据解析与判题输出:
- 多重错误优先级:针对格式错误、题目缺失、无答案、无效引用等情况分设优先级。在输出答案时,确保优先输出答案缺失的提示。
- 学号引用错误:答卷中的学号若不在学生信息列表中,需提示错误并跳过相关信息。
- 题目删除后的判题处理:设计处理逻辑,当题目被删除后,该题在试卷中的所有引用均需改为0分并提示题目无效。
3.1.3 细化异常处理:
- 错误格式信息:任何不符合格式要求的输入行均提示“wrong format”,并忽略。
- 无效试卷号和题号的检测:答卷和试卷若引用不存在的试卷号或题号,给出详细的错误提示。
- 题目删除处理:当题目被删除后,试卷中涉及该题的所有答案和内容均按无效题目处理,显示0分。
3.2 题量
- 最大题量:增加到题目类、试卷类、答卷类、学生类和删除信息类等多个类的设计。
- 实现内容:包括多重信息解析和判题逻辑、多重错误处理、无效引用提示、题目删除判0分等复杂逻辑。
3.3 难度
- 非常难:考验对面向对象设计、数据解析、异常处理和逻辑优先级处理的综合能力。需要确保复杂的异常处理逻辑覆盖到各种输入错误的场景,同时保证高效的数据管理和输出。
二、设计与分析
1. 题目一
1.1 类图设计
1.2 SourceMonitor 报表分析
- 项目目录:
D:\javaProject\PTA1\
- 项目名称:PTA1
- 检查点名称:Baseline
- 文件名称:
src\Main\Main.java
- 代码行数:131 行
- 语句数:75 个语句
- 分支语句百分比:9.3%,表示代码中分支语句(如
if
,switch
等)占总语句的比例。 - 方法调用语句:37 个方法调用语句。
- 代码注释百分比:21.4%,表示代码中注释行占总行数的比例。
- 类和接口数:1,表示该文件中定义了 1 个类或接口。
- 每类方法数:13,表示该类中定义了 13 个方法。
- 每方法平均语句数:5.15,表示平均每个方法包含 5.15 个语句。
- 最复杂方法的行号:6,表示
Main.main()
方法是最复杂的方法。 - 最大复杂度:4,表示代码中方法的最大复杂度为 4。
- 最深代码块的行号:34,表示最深代码块的行号。
- 最大代码块深度:4,表示代码块的最大嵌套深度。
- 平均代码块深度:1.17,表示平均代码块深度。
- 平均复杂度:4.00,表示所有方法的平均复杂度。
- 方法分布:最复杂方法在 1 个类中。
1.3 代码分析
1.3.1 类职责分工
- Main 类:负责管理整个程序的流程控制。
- QuestionItem 类:负责封装题目信息,并提供答案校验功能。
- Paper 类:封装了题目的集合,并提供增删和排序题目等操作。
- AnswerPaper 类:封装了答卷相关的逻辑,包括存储答案、判题和展示结果。
1.3.2 功能分析
- 输入读取:程序通过
Scanner
从控制台读取输入。用户首先输入题目数量,然后逐行输入题目,最后输入用户的答案。每道题目包含题号、题目内容和标准答案。 - 解析题目和答案:在循环中,程序通过解析每行输入的题目字符串,将题号、题目内容和答案提取出来。题目字符串的格式为:
#N: x #Q: content #A: answer
,程序根据分隔符#N: | #Q: | #A:
进行拆分。类似地,答案输入的格式是:#A: answer #A: answer ...
,程序根据#A:
进行拆分。 - 创建试卷对象 Paper:在
Paper
类中,程序使用一个Map<Integer, QuestionItem>
来存储题目,以便根据题号快速查找。程序将每个QuestionItem
添加到Paper
中。 - 创建答卷对象 AnswerPaper:
AnswerPaper
类中,程序接收Paper
对象,并保存用户的答案列表。同时,答卷中维护一个判题结果列表,用来存储每道题目判题的结果(true
或false
)。 - 判题:程序通过
checkAns
方法,遍历试卷中的所有题目,并调用QuestionItem
类的verifyAns
方法,逐个题目比较标准答案与用户的答案是否相同,将结果存储在resultList
中。 - 结果展示:程序提供两个展示功能:
- showQuesAndAns:输出题目内容和用户答案的对应关系。
- showResults:输出每道题的判题结果,结果为
true
或false
。
2. 题目二
2.1 类图设计
2.2 SourceMonitor 报表分析
- 项目目录:
D:\javaProject\PTA2\
- 项目名称:PTA2
- 检查点名称:Baseline
- 文件名称:
src\Main\Main.java
- 代码行数:194 行
- 语句数:142 个语句
- 分支语句百分比:13.4%,表示代码中分支语句(如
if
,switch
等)占总语句的比例。 - 方法调用语句:65 个方法调用语句。
- 代码注释百分比:1.5%,表示代码中注释行占总行数的比例,这表明代码的注释较少。
- 类和接口数:6,表示该文件中定义了 6 个类或接口。
- 每类方法数:3.83,表示平均每个类中有 3.83 个方法。
- 每方法平均语句数:4.04,表示平均每个方法包含 4.04 个语句。
- 最复杂方法的行号:81,表示
AnswerSheet.evaluateAnswers()
方法是最复杂的方法。 - 最大复杂度:7,表示代码中方法的最大复杂度为 7。
- 最深代码块的行号:91,表示最深代码块的行号。
- 最大代码块深度:4,表示代码块的最大嵌套深度。
- 平均代码块深度:1.94,表示平均代码块深度。
- 平均复杂度:1.87,表示所有方法的平均复杂度。
2.3 代码分析
2.3.1 类职责分工
- Main 类:负责程序的主流程控制。它从控制台读取输入,利用
InputHandler
对象解析输入的数据,并根据InputHandler
解析结果输出警告信息、判题结果等内容。 - Question 类:封装了单个题目的信息,包括题目编号、题目内容和标准答案。
Question
类的主要功能是提供访问题目信息的接口(getter 方法)。 - TestPaper 类:封装了试卷的信息,包括试卷编号和题目的分数分配。该类负责记录每个题目的分数,并提供获取试卷总分数的功能。
- AnswerChecker 类:专门负责验证用户的答案是否与标准答案一致。
AnswerChecker
类实现了答案比较的逻辑,符合单一职责原则。 - AnswerSheet 类:封装了答卷的相关信息,包括试卷编号、用户答案列表和题目顺序。它负责利用
AnswerChecker
对用户答案进行判题,并输出每个题目和用户答案的对应信息,以及总分。 - InputHandler 类:负责从输入数据中解析题目信息、试卷信息和答卷信息,并将这些数据保存到相应的集合中。此外,该类负责检查试卷的总分是否等于 100,并处理无效的试卷编号。
2.3.2 功能分析
-
输入读取:
- 程序通过
Scanner
从控制台读取输入,用户首先输入题目信息、试卷信息和答卷信息,最后输入end
表示输入结束。 - 每一行输入的前缀决定了它的类型:
#N:
表示题目信息,#T:
表示试卷信息,#S:
表示答卷信息。
- 程序通过
-
解析题目信息:
- 题目输入的格式为:
#N: x #Q: content #A: answer
。程序根据分隔符#Q:
和#A:
进行字符串拆分,提取题目的编号、内容和标准答案。 - 程序使用
parseQuestion
方法将每个题目解析为一个Question
对象,并将其保存到questionMap
中。
- 题目输入的格式为:
-
解析试卷信息:
- 试卷输入的格式为:
#T: paperId questionNumber-score questionNumber-score ...
。程序根据空格进行拆分,提取试卷编号和题目编号-分数对。 - 程序通过
parseTestPaper
方法将试卷编号和题目分数信息保存到TestPaper
对象中,并计算试卷的总分数。如果总分不为 100,则将该试卷编号加入paperIdsWithWrongTotal
列表中。
- 试卷输入的格式为:
-
解析答卷信息:
- 答卷输入的格式为:
#S: paperId #A: answer #A: answer ...
。程序提取答卷的试卷编号和用户的答案,并检查试卷编号是否有效。 - 程序使用
parseAnswerSheet
方法将答卷数据保存到AnswerSheet
对象中,并检查试卷编号的有效性。
- 答卷输入的格式为:
-
创建答卷对象 AnswerSheet:
- 程序在解析答卷信息时,创建
AnswerSheet
对象并将其与TestPaper
相关联。AnswerSheet
类封装了试卷编号、用户答案、题目顺序和答案检查器。
- 程序在解析答卷信息时,创建
-
判题:
- 程序调用
AnswerSheet
类的evaluateAnswers
方法进行判题。evaluateAnswers
方法根据用户的答案和题目的标准答案进行比较,利用AnswerChecker
来检查答案是否正确。 - 对于每道题目,
evaluateAnswers
方法将结果信息保存到resultMessages
列表中,同时根据正确性决定是否给该题目分数。
- 程序调用
-
结果展示:
- 程序在
evaluateAnswers
方法中,通过输出每道题目、用户答案和判题结果来展示判题信息。 - 另外,程序还输出每道题的得分,并计算总得分。
- 程序在
-
试卷总分和无效试卷检查:
- 程序通过
parseTestPaper
方法检查每个试卷的总分是否为 100。如果试卷的总分不为 100,则输出警告信息:alert: full score of test paper {paperId} is not 100 points
。 - 如果有无效的试卷编号,则程序输出统一的提示信息:
The test paper number does not exist
。
- 程序通过
3. 题目三
3.1 类图设计
3.2 SourceMonitor 报表分析
- 项目目录:
D:\javaProject\PTA3\
- 项目名称:PTA3
- 检查点名称:Baseline
- 文件名称:
src\Main\Main.java
- 代码行数:259 行
- 语句数:187 个语句
- 分支语句百分比:18.7%,表示代码中分支语句(如
if
,switch
等)占总语句的比例。 - 方法调用语句:88 个方法调用语句。
- 代码注释百分比:6.6%,表示代码中注释行占总行数的比例。
- 类和接口数:5,表示该文件中定义了 5 个类或接口。
- 每类方法数:4.00,表示平均每个类中有 4 个方法。
- 每方法平均语句数:5.55,表示平均每个方法包含 5.55 个语句。
- 最复杂方法的行号:168,表示
Main.parseAnswerSheet()
方法是最复杂的方法。 - 最大复杂度:5,表示代码中方法的最大复杂度为 5。
- 最深代码块的行号:140,表示最深代码块的行号。
- 最大代码块深度:4,表示代码块的最大嵌套深度。
- 平均代码块深度:1.95,表示平均代码块深度。
- 平均复杂度:2.05,表示所有方法的平均复杂度。
3.3 代码分析
3.3.1 类职责分工
- Main 类:负责管理整个程序的流程控制。包括解析输入、管理数据集合、输出处理结果,并协调
Question
、TestPaper
、Student
和AnswerSheet
类之间的操作。 - Question 类:负责封装题目信息,包含题目编号、题目内容、标准答案,以及标记题目是否已删除。提供格式化题目信息的功能和计算题目得分的功能。
- TestPaper 类:封装试卷信息,包括试卷编号、题目的分数分配和顺序号到题目 ID 的映射。负责添加题目、维护试卷总分、以及检查试卷总分是否为 100 分。
- Student 类:封装学生信息,包括学生编号和姓名。作为答卷的关联对象,代表每个学生。
- AnswerSheet 类:封装答卷信息,包括学生编号、试卷编号、以及题目顺序号和学生答案的映射。负责记录每个学生对每道题的回答。
3.3.2 功能分析
- 输入读取与解析:
- 程序通过
Scanner
从控制台读取输入,每次读取一行。根据行的前缀来判断输入的类型:#N:
表示题目信息,#T:
表示试卷信息,#X:
表示学生信息,#S:
表示答卷信息,#D:
表示删除题目。
- 程序通过
- 解析题目信息:
- 题目输入的格式为:
#N: id #Q: content #A: answer
。程序使用parseQuestion
方法,将题目的编号、内容和答案提取出来,并创建Question
对象存储到questions
映射中。如果题目格式不正确,则记录错误日志。
- 题目输入的格式为:
- 解析试卷信息:
- 试卷输入的格式为:
#T: testPaperId questionId-points ...
。程序使用parseTestPaper
方法解析试卷编号、题目编号和分数。 - 解析后,将每道题的顺序号、题目编号和分数映射存储到
TestPaper
对象中,并检查试卷总分是否为 100。
- 试卷输入的格式为:
- 解析学生信息:
- 学生输入的格式为:
#X: studentId name ...
。程序使用parseStudent
方法解析每个学生的编号和姓名,并创建Student
对象存储到students
映射中。
- 学生输入的格式为:
- 解析答卷信息:
- 答卷输入的格式为:
#S: testPaperId studentId #A: questionOrder-answer ...
。程序使用parseAnswerSheet
方法解析学生的答卷信息,记录每个题目的顺序号和对应的答案,并创建AnswerSheet
对象存储到answerSheets
列表中。 - 如果答卷格式不正确或没有找到对应的试卷编号,记录错误日志。
- 答卷输入的格式为:
- 题目删除:
- 删除题目输入的格式为:
#D:N-id
。程序通过deleteQuestion
方法,将Question
对象的isDeleted
标志位设为true
,表示该题目已删除。
- 删除题目输入的格式为:
- 判题与得分计算:
- 程序在
outputResults
方法中,首先检查所有试卷的总分是否为 100 分。如果不符合,则输出警告信息。 - 对于每个答卷,程序遍历
answerSheets
列表,依次检查题目的答案,计算得分,并输出结果。 - 逐题输出学生的答题结果,包括题目内容、学生答案、是否正确等信息,并输出每道题的得分。
- 计算并输出每个学生的总得分。
- 程序在
- 结果展示:
- 程序通过
outputResults
方法输出所有的错误日志和判题结果。包括题目格式错误、试卷分数不正确的警告、无效的试卷编号提示等信息。 - 对每个学生的答卷,程序输出学生的编号、姓名、各题得分和总分数。如果学生信息缺失,则提示学生未找到。
- 程序通过
三、采坑心得
1. 题目一
1.1 输入解析问题
在最初的实现中,我在解析题目和答案时,采用了简单的字符串分割方法(String.split
)。但是,由于题目和答案内容中可能包含空格、特殊符号等字符,这导致了在解析过程中,出现了以下两个主要问题:
- 问题 1:题目内容或答案中包含空格时,分割出现偏差。例如,输入题目
#N:1 #Q starting point of the Long March is #A
时,解析会错误地将#A:ruijin
分割成独立的两部分,导致获取标准答案时出错。 - 问题 2:输入顺序与编号无关,题目按编号顺序排列时未能正确排序。
解决方案
针对题目内容或答案中含空格的问题,我修改了分割的逻辑,采用正则表达式解析字符串,来更加灵活地提取内容:
String pattern = "#N:(\\d+) #Q:(.*?) #A:(.*)";
Matcher matcher = Pattern.compile(pattern).matcher(quesInput);
if (matcher.matches()) {
int qNum = Integer.parseInt(matcher.group(1).trim());
String qText = matcher.group(2).trim();
String qAns = matcher.group(3).trim();
// 创建题目对象
QuestionItem qItem = new QuestionItem(qNum, qText, qAns);
paper.addQues(qItem);
}
这里通过正则表达式 #N:(\\d+) #Q:(.*?) #A:(.*)
来提取题号、题目内容和答案,不再依赖固定的空格或分隔符,能够适应不同输入格式的情况。
针对题目编号的顺序问题,在获取所有题目后,我对题目列表进行了排序,以确保按编号的顺序输出题目:
qList.sort(Comparator.comparingInt(QuestionItem::getQNum)); // 按题号排序
1.2 答案的顺序与题目编号的对齐问题
在最初的实现中,我直接按照答案的输入顺序来对比题目和答案,忽略了题目编号与答案顺序可能不一致的情况。这在测试用例 5 中显现出来,输入顺序为:
#N:2 #Q:1+1= #A:2
#N:1 #Q:5+5= #A:10
#A:10 #A:2
这意味着,题号为 1 的题目答案应当为 10,而题号为 2 的题目答案为 2。
解决方案
为了解决这个问题,我采用了基于题目编号来映射答案的逻辑。具体地,通过一个 Map<Integer, String>
来存储题目编号与对应的答案,从而确保题目与答案的顺序对齐。相关代码如下:
Map<Integer, String> ansMap = new HashMap<>();
List<QuestionItem> qList = paper.getAllQues();
for (int i = 0; i < qList.size(); i++) {
QuestionItem qItem = qList.get(i);
int qNum = qItem.getQNum();
ansMap.put(qNum, ansList.get(i));
}
这样,通过题号来索引题目和答案,保证了输入顺序不影响最终结果的正确性。
2. 题目二
2.1 输入格式复杂度的增加
在答题判题程序-2 中,输入变得更加灵活,包括题目、试卷和答卷信息的混合输入。因此,直接读取输入并进行判断的方式需要重新设计。这部分的主要挑战在于:
- 问题 1:题目、试卷和答卷的混合输入顺序导致单一逻辑的解析会失效。例如,题目可以与试卷信息混合出现,增加了解析和存储的难度。
- 问题 2:题目编号不再严格连续,这意味着我们需要允许缺失的题目编号,并且无效题号不应影响程序的运行。
解决方案
为了应对这些问题,我引入了 InputHandler
类,用于将每一行输入分别解析并分类处理。代码片段如下:
if (inputLine.startsWith("#N:")) {
parseQuestion(inputLine);
} else if (inputLine.startsWith("#T:")) {
parseTestPaper(inputLine);
} else if (inputLine.startsWith("#S:")) {
parseAnswerSheet(inputLine);
}
这种处理逻辑能够应对输入顺序的随机性,并且通过 parseXXX
方法的拆分,使代码的可读性和灵活性大大增强。
2.2 试卷得分与警告信息
在实现过程中,试卷得分总和的检查成为一个需要特别注意的点。程序要求当试卷总分不等于 100 时输出警告信息,但这并不会影响程序的正常运行。因此,我设计了一个方法来计算试卷的总分并存储需要警告的试卷号。
- 问题 1:如果试卷得分总和为 100 时,我们忽略警告信息。
- 问题 2:如果得分总和不为 100,如何在程序的正确位置提示这一警告。
解决方案
通过为 TestPaper
类增加 getTotalScore
方法计算试卷总分,并在解析试卷信息时检查总分。代码片段如下:
if (testPaper.getTotalScore() != 100) {
paperIdsWithWrongTotal.add(paperId);
}
然后在主方法中遍历输出警告信息:
for (int paperId : inputHandler.getPaperIdsWithWrongTotal()) {
System.out.println("alert: full score of test paper " + paperId + " is not 100 points");
}
这样能够确保即使试卷总分不为 100,程序依然可以正常处理后续的题目和答案。
2.3 答卷解析与答案计分
由于答卷的信息与试卷号绑定,同时答卷的顺序必须与试卷中的题目顺序一致,这使得解析和验证答案的难度增加。尤其是题目和答案数量不匹配时,我们需要能够处理“缺失答案”或“多余答案”的情况。
- 问题 1:题目数量与答案数量不匹配时,缺失的答案需处理为“
answer is null
”。 - 问题 2:当答卷中包含无效试卷号时,需要输出错误信息。
解决方案
通过 AnswerSheet
类中保存答案顺序与题目编号对应关系,并在 evaluateAnswers
方法中根据顺序进行逐一判断:
for (int i = 0; i < questionOrder.size(); i++) {
int questionNumber = questionOrder.get(i);
String userAnswer = (i < answers.size()) ? answers.get(i) : "answer is null";
if (!questionMap.containsKey(questionNumber)) {
resultMessages.add("The question number " + questionNumber + " does not exist.");
scores.add(0);
continue;
}
// 处理答案...
}
此外,通过对答卷中的无效试卷号进行检测并设置标志位,在输出时提示:
if (inputHandler.invalidPaperExists()) {
System.out.println("The test paper number does not exist");
}
2.4 多样化的测试用例验证
在本次实现中,我设计了更加多样化的测试用例来验证程序的健壮性。主要包括以下几类:
- 混合输入的顺序验证:确保程序能够处理混乱的输入顺序,正确解析题目、试卷和答卷信息。
- 缺失题目和无效试卷号的处理:验证程序在题号缺失和无效输入情况下的正确处理和输出。
- 试卷分数警告:当试卷的总分不为 100 时,验证警告信息是否按要求输出。
测试结果示例
测试用例 5:
输入为:
#N:3 #Q:3+2= #A:5
#N:2 #Q:2+2= #A:4
#T:1 3-70 2-30
#S:1 #A:5 #A:22
#N:1 #Q:1+1= #A:2
end
输出结果为:
3+2=~5~true
2+2=~22~false
70 0~70
测试用例 7:
输入为:
#N:3 #Q:3+2= #A:5
#N:2 #Q:2+2= #A:4
#T:1 3-7 2-6
#S:1 #A:5 #A:22
#N:1 #Q:1+1= #A:2
#S:1 #A:5 #A:4
end
输出结果为:
vbnet
alert: full score of test paper1 is not 100 points
3+2=~5~true
2+2=~22~false
7 0~7
3+2=~5~true
2+2=~4~true
7 6~13
测试用例 9:
输入为:
#N:3 #Q:3+2= #A:5
#N:2 #Q:2+2= #A:4
#T:1 3-7 2-6
#S:3 #A:5 #A:4
end
输出结果为:
alert: full score of test paper1 is not 100 points
The test paper number does not exist
心得体会
-
合理的类划分与逻辑分离:通过将题目、试卷、答卷和输入解析等功能分别封装在独立的类中,使得代码更加模块化,增强了代码的可读性和可维护性。尤其是针对复杂逻辑的处理,通过类的封装能够显著减少耦合,提高代码复用性。
-
输入与边界条件的严格处理:在复杂输入和边界条件的处理上,这次的实现让我深刻体会到全面考虑和充分测试的重要性。每一类信息的解析必须能够应对多种输入形式,并且在设计解析逻辑时,需要充分考虑到空值和无效值等特殊情况。
3. 题目三
3.1 输入格式的严格检查
由于本次题目要求处理的输入格式更加复杂,例如题目信息、试卷信息、答卷信息、删除信息等各自有着独特的格式要求,同时对顺序、空格、符号等进行了详细规定。因此,简单的字符串分割方法可能会导致输入解析出错,或无法满足格式的严格性。
问题表现
- 问题 1:如果输入不符合格式要求,例如关键字拼写错误或多余空格,则会导致解析失败。
- 问题 2:删除题目信息、学生信息等新增加的输入类型,也要求同样严格的格式校验。
解决方案
为了应对这些问题,我采用了逐步解析和精确检查的方式,同时在解析每一行时做了明确的异常处理。每一个输入处理函数都有详细的检查逻辑,并将解析错误归入到错误日志中。例如,在解析题目时,加入了更精确的检查:
if (parts.length < 3 || !parts[1].startsWith("#Q:") || !parts[2].startsWith("#A:")) {
throw new Exception("wrong format");
}
此外,在所有输入解析中,增加了对输入格式的异常捕获并记录错误信息:
try {
system.parseQuestion(line);
} catch (Exception e) {
system.errorLogs.add("wrong format:" + line);
}
3.2 题目删除与无效题目的处理
在本次实现中,需要对题目进行删除,并且在输出答案时以“the question X invalid~0”的格式进行提示。这要求在删除题目后,不影响试卷的使用,同时要保持答案输出的连贯性。
问题表现
- 问题 1:删除题目后,仍然需要在试卷中显示该题目失效,并将分数计为 0。
- 问题 2:当试卷引用的题目本身无效时,需要正确提示。
解决方案
为了应对这些问题,我在 Question
类中增加了一个 isDeleted
字段,用于标识题目是否被删除。并在 getFormattedQuestion
方法中,检查题目的删除状态,从而输出失效信息:
if (isDeleted) {
return "the question " + id + " invalid~0";
}
同时,在 getScore
方法中,确保被删除的题目无法得分:
if (isDeleted || studentAnswer == null || !answer.equals(studentAnswer.trim())) {
return 0;
}
3.3 学生信息与学号验证
在本次题目中,增加了对学生信息的处理,需要确保答卷中的学号信息与学生列表中的学号相匹配。同时,如果学号未在学生列表中,则需要在判分信息中提示“学号 not found”。
问题表现
- 问题 1:答卷信息中的学号可能与学生信息中的学号不匹配。
- 问题 2:在输出判分信息时,需要根据学生信息的有效性输出不同的提示。
解决方案
为了解决学号验证问题,我在 parseStudent
方法中进行了学生信息的解析与存储。在处理答卷时,通过判断学号是否在学生列表中存在,输出不同的信息:
Student student = students.get(answerSheet.studentId);
boolean studentNotFound = (student == null);
在最终输出时,针对未找到的学号做提示:
if (!studentNotFound) {
System.out.println(answerSheet.studentId + " " + student.name + ": " + scoreOutput.toString().trim() + "~" + totalScore);
} else {
System.out.println(answerSheet.studentId + " not found");
}
3.4 多种错误优先级的处理
在本次实现中,不同类型的错误有不同的优先级,例如“答案为空”的优先级最高。如果某道题目同时出现多种问题,我们需要确保优先处理“答案为空”的情况。
问题表现
- 问题 1:在答题信息中,如果答案为空、题目被删除或题目引用无效,需要按优先级输出正确的提示信息。
- 问题 2:每种错误信息的输出需要保持连贯。
解决方案
在输出答题信息时,我首先检查答案是否为空,如果为空则直接输出提示信息。否则,再依次检查题目是否被删除或引用无效:
if (studentAnswer == null) {
answerOutput.append("answer is null\n");
scoreOutput.append("0 ");
} else if (question == null) {
answerOutput.append("non-existent question~0\n");
scoreOutput.append("0 ");
} else {
answerOutput.append(question.getFormattedQuestion(studentAnswer)).append("\n");
int score = question.getScore(studentAnswer, entry.getValue());
scoreOutput.append(score).append(" ");
totalScore += score;
}
测试结果示例
测试用例 1:简单输入不含删除信息:
#N:1 #Q:1+1= #A:2
#T:1 1-5
#X:20201103 Tom
#S:1 20201103 #A:1-5
end
输出结果:
alert: full score of test paper1 is not 100 points
1+1=~5~false
20201103 Tom: 0~0
测试用例 5:含错误格式输入、无效题目引用、有效删除信息:
#N:1 +1= #A:2
#N:2 #Q:2+2= #A:4
#T:1 1-5 2-8
#X:20201103 Tom-20201104 Jack-20201105 Www
#S:1 20201103 #A:1-5 #A:2-4
#D:N-2
end
输出结果:
wrong format:#N:1 +1= #A:2
alert: full score of test paper1 is not 100 points
non-existent question~0
the question 2 invalid~0
20201103 Tom: 0 0~0
测试用例 9:无效的学号引用:
#N:1 #Q:1+1= #A:2
#T:1 1-5
#X:20201106 Tom
#S:1 20201103 #A:1-5 #A:2-4
end
输出结果:
alert: full score of test paper1 is not 100 points
1+1=~5~false
20201103 not found
四、改进建议
1. 题目一
1.1 提高输入解析的鲁棒性与灵活性
-
现状:在第一个题目中,输入解析主要通过字符串分割来完成,但这种方法在处理复杂输入时可能存在局限性。例如,当输入内容中包含特殊字符或空格时,解析结果可能不如预期。
-
改进建议:可以使用更为灵活的正则表达式来解析输入,避免因简单的分割逻辑而导致的解析错误。正则表达式可以帮助我们更清晰地定义和识别输入格式的模式,从而提升解析的准确性。
-
改进方法:
String pattern = "#N:(\\d+) #Q:(.*?) #A:(.*)";
Matcher matcher = Pattern.compile(pattern).matcher(quesInput);
if (matcher.matches()) {
int qNum = Integer.parseInt(matcher.group(1).trim());
String qText = matcher.group(2).trim();
String qAns = matcher.group(3).trim();
// 创建题目对象并添加到试卷中
}
这种改进能够有效解决空格或特殊字符导致的解析偏差,提高程序的鲁棒性。
1.2 抽象出题目顺序与编号的映射关系
-
现状:当前代码假设题目顺序与编号是等价的,而题号和输入顺序并没有直接关联。这样会在后续添加功能或修改代码时带来潜在的复杂性。
-
改进建议:可以引入一个专门的映射来处理题目顺序与编号的关系,从而能够更灵活地组织题目数据。这种方式在后续扩展(如题目删除或动态调整题目顺序)时,能够更清晰和简洁。
例如:
private Map<Integer, Integer> questionOrderToNumber = new LinkedHashMap<>();
这种映射能够在解析题目和答案时,准确定位题目的顺序和编号关系,便于程序管理和维护。
1.3 题目类与试卷类之间的解耦
-
现状:题目类与试卷类在当前代码中相互依赖较多,导致试卷类承担了较多的题目管理责任,使得程序结构的耦合性较高。
-
改进建议:可以将题目管理的逻辑从试卷类中分离出来,单独创建一个
QuestionManager
类来专门处理题目的添加、删除、更新等操作。好处:
- 试卷类的职责更加单一,专注于试卷逻辑的处理。
- 题目管理逻辑统一在一个类中,易于扩展题目相关功能。
例如,可以创建一个
QuestionManager
类,如下:class QuestionManager { private Map<Integer, QuestionItem> questions; public void addQuestion(int number, String text, String answer) { questions.put(number, new QuestionItem(number, text, answer)); } public QuestionItem getQuestion(int number) { return questions.get(number); } public void deleteQuestion(int number) { questions.remove(number); } }
1.4 优化数据结构与算法
-
现状:在当前的实现中,程序通过多个
ArrayList
和HashMap
来管理和查找数据,可能在查找和遍历较大规模数据时效率不高。 -
改进建议:根据具体的需求,考虑使用更适合的集合类型。例如,可以引入
TreeMap
来替代HashMap
,以便在需要时能够自动对题目进行排序。对于查找频繁的题目,可以引入索引或使用高效的数据结构来加速访问。
2. 题目二
2.1 提高输入解析的鲁棒性与灵活性
-
现状:输入信息的解析逻辑主要依赖字符串分割,程序中各类输入(题目信息、试卷信息、答卷信息等)解析部分较为冗长,增加了维护的复杂性,并且对格式的容错性不强。
-
改进建议:为了解决这个问题,建议引入正则表达式统一管理各类输入的解析,并使用一个专门的解析器类(
InputParser
)来处理输入的解析逻辑。这样可以将输入的解析逻辑从主要流程中解耦,简化主程序的代码逻辑,提高输入的容错性。示例代码:
public class InputParser { public Question parseQuestion(String line) { // 使用正则表达式进行解析 Pattern pattern = Pattern.compile("#N:(\\d+) #Q:(.*?) #A:(.*)"); Matcher matcher = pattern.matcher(line); if (matcher.matches()) { int id = Integer.parseInt(matcher.group(1).trim()); String content = matcher.group(2).trim(); String answer = matcher.group(3).trim(); return new Question(id, content, answer); } else { throw new IllegalArgumentException("Invalid question format: " + line); } } // 解析试卷、答卷、删除题目信息等类似逻辑 }
这种设计使得解析逻辑集中管理,便于修改和扩展。
2.2 优化题目、试卷和答卷之间的关联
-
现状:程序中,题目、试卷和答卷之间的关联主要依赖编号的映射。这种简单的映射方法虽然能够满足题目二的需求,但在更复杂的情况(如动态题目管理、题号混乱等)下可能导致较多的手动关联。
-
改进建议:通过引入更灵活的数据结构来管理题目和试卷之间的关系,如在
TestPaper
类中引入List
或Map
结构来明确地记录题目的顺序号与编号映射。这样可以简化题目添加、删除和关联的逻辑,提高代码的可扩展性和维护性。示例代码:
class TestPaper { private int id; private Map<Integer, Integer> questionOrderToId; // 顺序号 -> 题目编号 public TestPaper(int id) { this.id = id; this.questionOrderToId = new LinkedHashMap<>(); // 保证顺序 } public void addQuestion(int order, int questionId) { this.questionOrderToId.put(order, questionId); } // 获取题目的顺序和对应编号的方法 }
2.3 改进答卷和题目的关联方式
-
现状:在处理答卷时,题目的编号和顺序号的对应关系较为模糊,这使得解析和关联答卷的逻辑较为复杂,特别是在顺序和编号混乱或缺失的情况下。
-
改进建议:可以为答卷信息专门创建一个
AnswerManager
类,该类负责根据试卷的题目顺序来关联答案。通过使用Map
和顺序的结合来清晰表达题目与答案的对应关系。示例代码:
public class AnswerManager { private Map<Integer, String> answers; // 顺序号 -> 答案 public void addAnswer(int order, String answer) { this.answers.put(order, answer); } public String getAnswer(int order) { return answers.getOrDefault(order, "answer is null"); } }
3. 题目三
3.1 模块化的输入解析与验证
-
现状:在现有实现中,输入的解析和格式验证逻辑相对分散,程序的各部分独立解析输入的方式增加了代码的冗余性和复杂度。此外,错误提示的逻辑在各个部分中重复出现。
-
改进建议:引入一个独立的
InputParser
类来集中管理所有输入解析逻辑和格式验证,确保输入解析的统一性和简洁性。同时,将错误验证逻辑单独抽离到Validator
类中,使得各部分的错误检查能够集中处理,并提高复用性。示例代码:
public class InputParser { public Question parseQuestion(String line) throws IllegalArgumentException { Pattern pattern = Pattern.compile("#N:(\\d+) #Q:(.*?) #A:(.*)"); Matcher matcher = pattern.matcher(line); if (matcher.matches()) { int id = Integer.parseInt(matcher.group(1).trim()); String content = matcher.group(2).trim(); String answer = matcher.group(3).trim(); return new Question(id, content, answer); } else { throw new IllegalArgumentException("Invalid question format: " + line); } } // 其他输入解析逻辑类似处理 }
Validator 类:
public class Validator { public void validateStudentInfo(String studentId, Map<String, Student> students) throws IllegalArgumentException { if (!students.containsKey(studentId)) { throw new IllegalArgumentException("Student ID not found: " + studentId); } } // 其他验证逻辑 }
3.2 抽象题目与试卷之间的关系
-
现状:当前的程序中,题目、试卷和答卷之间的关系主要通过 ID 来维护,这种方式在面对多层级的关联关系时显得较为脆弱。此外,试卷中的题目被删除后,程序需要特别处理这些题目的状态。
-
改进建议:引入一个
QuestionManager
类和TestPaperManager
类来分别管理题目和试卷的逻辑,从而将题目管理、试卷管理与主流程逻辑解耦。对于题目删除操作,可以为Question
类增加一个状态字段,专门记录题目的有效性状态。示例代码:
public class QuestionManager { private Map<Integer, Question> questions; public void addQuestion(int id, String content, String answer) { questions.put(id, new Question(id, content, answer)); } public void deleteQuestion(int id) { if (questions.containsKey(id)) { questions.get(id).setDeleted(true); } } public Question getQuestion(int id) { return questions.get(id); } } public class TestPaperManager { private Map<Integer, TestPaper> testPapers; public void addTestPaper(int id, Map<Integer, Integer> questionOrderToId, Map<Integer, Integer> questionsAndPoints) { TestPaper testPaper = new TestPaper(id); // 添加题目到试卷 testPapers.put(id, testPaper); } }
3.3 增强错误处理与提示功能
-
现状:在现有程序中,错误信息的提示较为简单,且未能有效解耦处理逻辑,这导致在处理复杂情况(如无效学号、无效题目等)时代码较为冗杂。
-
改进建议:可以引入一个
ErrorManager
类来集中管理所有的错误提示,并通过定义错误优先级来处理多重错误的情况。此外,可以将错误提示类型抽象化为一个枚举,便于程序的扩展和维护。示例代码:
public class ErrorManager { private List<String> errorLogs = new ArrayList<>(); private Map<ErrorType, String> errorMessages; public void logError(ErrorType type, String details) { errorLogs.add(errorMessages.get(type) + ": " + details); } public void outputErrors() { for (String error : errorLogs) { System.out.println(error); } } }
通过使用
ErrorType
枚举,可以更明确地区分不同类型的错误,且在错误类型增加时,只需在枚举中添加新的错误类型。
3.4 优化学生信息与答卷的关联管理
-
现状:在处理学生信息时,学生和答卷的关联主要通过简单的 ID 映射来实现,当面对复杂的学生和答卷关联逻辑时,这种方式可能难以扩展。
-
改进建议:通过引入
StudentManager
类来管理所有的学生信息,同时为每个学生建立一张答卷列表,以便统一管理和查找学生及其答卷信息。这样可以简化答卷与学生的查找逻辑。示例代码:
public
class StudentManager {
private Map<String, Student> students;
public void addStudent(String studentId, String name) {
students.put(studentId, new Student(studentId, name));
}
public Student getStudent(String studentId) {
return students.get(studentId);
}
}
#### 3.5 **增加单元测试与代码复用**
- **现状**:当前的代码实现未能有效引入单元测试,导致代码的正确性主要依赖于手动验证,这样的验证方式较为低效。
- **改进建议**:在改进代码时,建议为每一个主要功能(如题目管理、试卷管理、答卷验证等)编写单元测试。通过使用 JUnit 等测试框架,可以有效提升代码的可靠性,并减少后续修改或扩展时可能引入的错误。
**示例代码**:
```java
@Test
public void testDeleteQuestion() {
Question question = new Question(1, "1+1=", "2");
question.setDeleted(true);
assertTrue(question.isDeleted());
}
五、总结
1. 所学内容的总结与收获
通过这三次编程题目的逐步深入,我逐渐掌握并提升了以下几个方面的技能:
面向对象编程的理解与实践:
- 类的封装与职责分配:每个题目都要求我们进行合理的类设计,学会了将题目、试卷、答卷等功能封装到独立的类中,明确每个类的职责,使得代码的可读性、复用性和扩展性得到提升。
- 模块化设计:我们在编程过程中通过拆分不同的功能模块,如题目解析、试卷管理、答卷处理、错误提示等,从而减少各模块之间的耦合度,使得代码的整体结构更加清晰和易于维护。
数据结构与算法的应用:
- 灵活的数据存储方式:通过使用
HashMap
、ArrayList
、TreeMap
等不同的集合结构,我们学会了在处理大量数据时选择合适的数据结构,以提高程序的效率和鲁棒性。 - 问题与数据间的映射:通过三次题目集的练习,我进一步理解了如何建立数据间的映射关系,并灵活应用这一技能来管理试卷中的题目与分值、题目与答案的对应关系。
输入解析与格式验证的深入理解:
- 正则表达式的使用:题目中输入格式的多样性和复杂性促使我们采用正则表达式来进行灵活的字符串匹配,从而提高了输入解析的准确性。
- 输入容错性和错误处理的能力:在实现过程中,特别是第三个题目,要求我们不仅要正确解析输入,还需要处理各种可能的输入错误,并在发生错误时给出具体且友好的提示信息。这种要求极大地提升了我们对代码鲁棒性的重视和对错误处理的实践能力。
软件设计与代码优化的经验:
- 可维护性与扩展性思维:通过三次题目集的编程练习,我学会了如何优化代码结构以提高其可扩展性。特别是在第三个题目中,引入了专门的管理类(如
ErrorManager
、QuestionManager
等),这些设计使得程序更容易进行后续的功能扩展和维护。 - 面向单元测试的开发实践:从第三个题目开始,我逐渐意识到单元测试的重要性,开始采用测试驱动开发(TDD)的思路,通过 JUnit 等测试框架编写单元测试,这极大地提高了代码的可靠性和信心。
2. 需要进一步学习与研究的领域
复杂数据结构与算法的深度理解:
在三次题目集的编程过程中,我逐步接触了 Map
、List
、Set
等常见数据结构的灵活应用。然而,在更高复杂度的题目中(如图结构、树结构等),数据的关联与管理变得更加复杂,因此我还需要进一步深入学习复杂数据结构的设计与应用。
正则表达式与字符串处理的进阶技巧:
虽然通过本阶段的练习,我对正则表达式有了一定的掌握,但在处理更复杂的输入场景时,正则表达式的设计与优化仍然是一个挑战。因此,我需要进一步学习正则表达式的高级特性及其在数据验证中的应用。
软件设计模式的学习:
在三次题目集中,我逐渐理解了面向对象,然而在面对更复杂的应用场景时,设计模式的掌握将会极大地提高我的代码设计能力。因此,我希望能够系统学习常见的设计模式,如工厂模式、观察者模式、策略模式等,应用到实际编程中。
3. 对课程与教学的改进建议
课程的整体设计很好,我没有特别好的建议。对我自己而言,我需要继续多写代码,多学习知识,不断提升自己的编程能力。