南昌航空大学 22207208-贺凯凯 第一次总结性Blog
一,前言
本项目中我们实现了一个模拟在线答题判分的程序,分为三次迭代,逐步扩展功能和复杂度。通过三次题目集的练习,我们从基本的答题判分系统开始,逐渐加入了多试卷、多学生、多题目管理等元素,最终形成一个多功能、多用户的答题管理系统。每个题目集中涉及的知识点包括输入处理、字符串解析、数据结构设计等,难度也从单一判题逐渐增加到复杂的试卷和学生信息的管理。题目数量递减,由5道题目递减到3道题目,但难度逐步递增。
二,设计与分析
1.题目集1第1题 设计一个风扇Fan类
本题要求我们设计一个Fan类来表示风扇的各项属性和行为。我们将风扇的速度、是否开启、半径、颜色作为数据成员,并提供相应的构造方法、访问器、修改器以及字符串输出方法。下面是主要的设计和分析:
1.类的常量:定义SLOW、MEDIUM和FAST三个常量,分别用1、2、3表示不同的速度。这使得速度有更清晰的表达方式,便于调用。
2.数据成员:
speed:整型,表示风扇的速度,默认为SLOW。
on:布尔型,表示风扇的开关状态,默认为false。
radius:双精度浮点型,表示风扇的半径,默认为5。
color:字符串类型,表示风扇的颜色,默认为white。
3.构造方法:
无参构造方法:初始化风扇为默认状态。
有参构造方法:接收速度、开关状态、半径和颜色参数,自定义风扇属性。
4.访问器和修改器:提供每个数据成员的get和set方法,使数据成员符合封装性原则。
5.toString() 方法:根据风扇是否开启返回描述信息。如果风扇开启,返回速度、颜色、半径及“fan is on”;如果风扇关闭,返回速度、颜色、半径及“fan is off”。
6.测试用例:要求创建两个风扇对象,一个是无参构造的默认风扇Fan1,另一个是根据用户输入的属性创建的自定义风扇Fan2,并通过调用toString方法输出风扇信息。
类图
7.注意事项
需要注意toString()方法中的条件判断,确保风扇在不同状态下输出的描述信息符合题目要求。
构造方法中的参数应当被适当地初始化到类的相应字段中,并考虑到非法输入的检查,例如速度的值限制在1到3之间。
注意设计输出格式,使其与题目样例严格一致,避免因格式错误影响测试通过率。
8.主要执行代码
// 创建风扇对象并从用户输入中获取风扇的属性
Fan fan1 = new Fan();
Scanner scanner = new Scanner(System.in);
int speed = scanner.nextInt();
boolean on = scanner.nextBoolean();
double radius = scanner.nextDouble();
scanner.nextLine();
String color= scanner.next();
// 使用用户输入的属性创建第二个风扇对象
Fan fan2 = new Fan(speed,on,radius,color);
// 输出两个风扇的信息
System.out.println("-------\nDefault\n-------");
System.out.println(fan1.tostring());
System.out.println("-------\nMy Fan\n-------");
System.out.println(fan2.tostring());
2.题目集1第5题 答题判题程序-1
题目要求设计一个简易的答题系统,接收题目信息和用户答案,进行判题并输出结果。代码主要分为几个关键部分,包括读取题目信息、保存答题信息、判题、以及输出结果。
- 需求分析
输入:
题目数量:一个整数。
题目内容:每个题目包含题号、题目内容和标准答案,顺序不定。
答题信息:每题的答案,顺序与题目题号对应,后接"end"表示结束。
输出:
题目与用户答案:按照题目内容和用户答案顺序输出。
判题结果:输出“true”或“false”表示用户答案的正确性,顺序与题目题号一致。
- 代码结构与功能
代码通过几个类来管理题目和答案信息。
Main 类:主程序逻辑,负责读取输入并进行初始化。
Paper 类:用来存储所有题目。
Question 类:表示一个题目,包含题号、题目内容、标准答案,以及判题方法。
AnswerSheet 类:存储用户的答题信息并负责判题和输出。
- 代码详解
Main 类
Main类的main方法首先读取题目数量,并使用循环读取每道题目内容。
使用正则表达式拆分输入字符串,解析题号、题目内容、和标准答案,并添加到Paper实例中。
读取用户的答题信息,直到读取到"end"。将用户的答案与题号对应,存储在AnswerSheet实例中。
Paper 类
Paper类维护一个HashMap,存储每道题的题号和题目信息。
提供addQuestion方法用于保存题目,getAllQuestions方法用于获取所有题目。
Question 类
Question类用于存储题目的内容、标准答案及相关方法。
isStandardAnswer方法用于判断用户的答案是否正确.
AnswerSheet 类
AnswerSheet类包含用户的答题信息和判题结果。
addAnswer方法接收用户答案,存储到HashMap中。
judges方法遍历所有题目,并通过judge方法逐题判定正误,将结果存入jud列表。
output方法负责格式化输出题目内容、用户答案和判题结果。
-
关键逻辑
题目解析:使用正则表达式解析题目内容,split("#N:|#Q:|#A:")从原始字符串中提取题号、题目内容和标准答案。
判题:通过Question类中的isStandardAnswer方法判定每道题目的正误,结果存入List。
输出:output方法按要求格式化输出,每道题目和答题信息逐行显示,判题信息以“true”和“false”表示,按用户输入的顺序。
// 遍历试卷中的所有问题并对用户的答案进行判断
for (Question q : paper.getAllQuestions()) {
String givenAnswer = answer.get(q.getNum());//用户答案
jud.add(judge(givenAnswer,q));
} -
类图
-
踩坑心得
在解析题目内容和答题信息时,使用正则表达式拆分字符串。若输入格式不规范(如多余空格、标记位置不对等),可能导致解析失败或抛出异常。
解决方案:
在使用正则表达式解析输入内容时,要确保去除多余空格,并捕获异常。trim()可以清除多余空格,同时增加异常处理,确保即便输入格式不严格,也能处理。
// 1. 读取题目数量
int questionCount = Integer.parseInt(scanner.nextLine().trim());
Paper paper = new Paper();
for (int i = 0; i < questionCount; i++) {
String line = scanner.nextLine().trim();
// 使用正则表达式解析题目信息
String[] parts = line.split("#N:|#Q:|#A:");
int num = Integer.parseInt(parts[1].trim());
String question = parts[2].trim();
String standardAnswer = parts[3].trim();
paper.addQuestion(num, question, standardAnswer);
} -
改进建议
对于答案判断部分的改进,去除冗余方法:原来的 judge 方法只是调用 isStandardAnswer 方法,因此直接在 judges 方法中调用 isStandardAnswer 可以减少方法调用的开销。
添加错误处理:在判断答案的同时增加了对 givenAnswer 是否为空的判断,以避免潜在的空指针异常。
public void judges() {
// 遍历试卷中的所有问题并对用户的答案进行判断
for (Question q : paper.getAllQuestions()) {
String givenAnswer = answer.get(q.getNum());//用户答案
jud.add(judge(givenAnswer,q));
}
}
public boolean judge(String answer,Question q){
return q.isStandardAnswer(answer);
}
改进后
public void judges() {
// 遍历试卷中的所有问题并对用户的答案进行判断
for (Question q : paper.getAllQuestions()) {
String givenAnswer = answer.get(q.getNum()); // 用户答案
// 使用安全的判断机制,避免空指针异常
jud.add(givenAnswer != null && q.isStandardAnswer(givenAnswer));
}
}
3.题目集2第4题
本题基于前题(答题判题程序-1)的基础上进行扩展。核心任务是设计一个答题判题程序,增加试卷(TestPaper)和答卷(AnswerSheet)功能,以便通过多种输入信息动态生成考试题目、计算分数并输出判题结果。
1.主要设计:
输入信息的解析:新增了试卷信息(包含试卷号和题目分值)和答卷信息(包含试卷号和答案),并要求能处理题目、试卷、答卷混合顺序输入,支持题号缺失和分值不满100分的试卷。
判题和评分的灵活性:题目支持缺失题号,答卷支持题目答案数量与试卷不一致。分值不满100的试卷会触发警告但不影响正常判分。
面向对象设计的扩展性:为了适应新增的试卷和答卷信息,设计了题目类(Question)、试卷类(TestPaper)、答卷类(AnswerSheet),各类职责单一,方法清晰,便于维护和扩展。
2. 需求分析
输入:
题目信息:每行包含题号、题目内容和标准答案,顺序不定,题号可能缺失。
格式:#N:题目编号 #Q:题目内容 #A:标准答案
样例:#N:1 #Q:1+1= #A:2
试卷信息:每行代表一张试卷,包含试卷号、题目编号和题目分值。
格式:#T:试卷号 题目编号-题目分值
样例:#T:1 1-5 2-8
答卷信息:每行包含一个试卷的答案,包含试卷号和该试卷题目的答案。
格式:#S:试卷号 #A:答案内容
样例:#S:1 #A:5 #A:22
输出:
试卷总分警示:若试卷总分不等于100分,则输出提示信息。
格式:alert: full score of test paper 试卷号 is not 100 points
题目与用户答案:按照题目顺序输出题目内容、用户答案和判题结果。
格式:题目内容~答案~判题结果(true/false)
判分信息:输出每道题目的得分和总分,按照输入顺序排列。
格式:题目得分1 题目得分2 ... ~ 总分
错误的试卷号提示:如果答案信息中的试卷号找不到,输出错误提示。
格式:the test paper number does not exist
- 代码结构与功能
代码采用面向对象的设计,共由四个主要类组成:Main 类、Question 类、TestPaper 类、AnswerSheet 类。
Main 类
主程序逻辑:负责读取题目信息、试卷信息、答卷信息的输入,对数据进行解析和初始化。
输入数据解析:根据前缀#N:, #T:, #S:判断信息类型并调用相应解析方法。
输出数据格式化:判定试卷总分,输出判题结果和答题总分。
Question 类
功能:表示一道题目,包含题号、题目内容和标准答案。
方法:
isCorrectAnswer(String answer):判断用户答案是否正确。
TestPaper 类
功能:存储一张试卷的信息,包括试卷号和题目编号、题目分值。
方法:
addQuestion(int num, int score):添加题目及其分值。
isFullScoreValid():判断试卷总分是否为100分。
AnswerSheet 类
功能:存储答卷信息并负责判分与输出结果。
方法:
judgeAnswers():判定答案的正确性并输出结果。
calculateScore():计算每个题目的得分并输出总分
-
边界条件
单个题目与试卷:确保系统能够处理单一题目和单一试卷的情况。
题号缺失:支持题号非连续的输入,并保证按顺序输出。
答案缺失:答卷中的答案数量少于试卷中的题目数量时,输出“answer is null”并得0分。
多余答案:答卷中的答案多于试卷中的题目数量时,忽略多余的答案。
无效试卷号:答卷中的试卷号不存在时,输出错误提示“the test paper number does not exist”。 -
类图
-
踩坑心得
1. 对于输入的题目#N:1 #Q:1+1= #A:2,以空格分隔解析,出现非零返回错误
if(input.startsWith("#N")){
String[] parts = input.split(" ");//这里
int questionNum = Integer.parseInt(parts[0].split(":")[1]) ;
String question = parts[1].split(":")[1];
String standardAnswer = parts[2].split(":")[1];
paper.addQuestion(questionNum,question,standardAnswer);
![](https://img2024.cnblogs.com/blog/3545753/202410/3545753-20241025234742927-467378645.png)
改进:
if(input.startsWith("#N")){
String[] parts = input.split("#");
int questionNum = Integer.parseInt(parts[1].split(":")[1].trim()) ;
String question = parts[2].split(":")[1].trim();
String standardAnswer = parts[3].split(":")[1].trim();
paper.addQuestion(questionNum,question,standardAnswer);
2. 获取学生答案时越界
String studentAnswer = as.getAnswers().get(i);
![](https://img2024.cnblogs.com/blog/3545753/202410/3545753-20241026000100875-1770837809.png)
使用三元运算符:在获取学生答案时,使用三元运算符来避免数组越界,同时提升代码的可读性。
String studentAnswer = (i < as.getAnswers().size()) ? as.getAnswers().get(i) : ""; // 防止越界
3. 对于链表没有空值判断处理
String StandardAnswer = paper.getQuestions().get(question_num).getStandardAnswer();
![](https://img2024.cnblogs.com/blog/3545753/202410/3545753-20241026001022437-941882956.png)
修改 String standardAnswer = currentQuestion != null ? currentQuestion.getStandardAnswer() : null;
String userAnswer = (i < as.getAnswers().size()) ? as.getAnswers().get(i) : null;
-
改进建议
1.抽取输入解析的方法:
建议将题目解析、分数解析和答卷解析逻辑分别封装成独立的方法,如 parseQuestion(), parseScore(), parseAnswerSheet()。
这样主方法 main() 中的 while 循环可以更简洁,代码逻辑更清晰。
2.简化分数判断逻辑:
将 scoreT 中判断总分是否为100的逻辑提取为 checkTotalScore() 方法,使得 main() 更加简洁。
添加自定义异常,如 ScoreOutOfBoundsException,在总分不为100时抛出自定义异常。
3.改进答卷处理逻辑:
将答卷中逐题判分的逻辑抽取到 AnswerSheet 类中,如添加 gradeAnswerSheet(scoreT, Paper paper) 方法。
这样可以在 AnswerSheet 中管理自己的评分逻辑,不需要在 main() 中详细管理分数的判定。
4.改进分数计算的防御性编程:
代码2在计算每题分数时,防止了数组越界,但可以进一步加强判空处理,避免 NPE 异常。
5.统一输出格式处理:
将 System.out.println() 的格式化输出集中在一个方法中,通过方法参数控制不同输出格式,使输出一致且易维护。
如:
while (true) {
String input = scanner.nextLine();
if (input.equals("end")) break;if (input.startsWith("#N")) { parseQuestion(input, paper); } else if (input.startsWith("#T")) { parseScore(input, scoreHash); } else if (input.startsWith("#S")) { parseAnswerSheet(input, answerSheets); } }
parseQuestion(String input, Paper paper):
parseScore(input, scoreHash);
parseAnswerSheet(input, answerSheets);
6.多余代码优化:
在 scoreT 和 AnswerSheet 中改进了对试卷号和题号的处理,移除了不必要的代码。
7.输出优化
if (scores.isEmpty()) {
System.out.println("No scores available.");
} else {
System.out.print(String.join(" ", scores.stream().map(String::valueOf).toArray(String[]::new)) + "~" + total);
}
减少不必要的循环:使用 String.join 和流 API(stream())将 scores 中的元素直接转换为字符串并连接,而不是使用 for 循环逐个添加。
去除冗余代码:通过一次性地处理输出,避免了在循环中多次调用 print 方法。
增加错误处理:在输出之前,检查 scores 列表是否为空,如果为空则输出相应的提示信息。
4.题目集3第3题 答题判题程序-3
该题目要求构建一个答题判题程序,用于模拟小型测试的答题和判分过程。包括题目信息管理,试卷管理,学生信息管理,答卷判题,题目删除处理和错误提示。
构建一个功能完备的答题判题系统,通过类与方法的封装,对输入数据进行解析、验证和判分,并根据不同情况输出判题结果、得分统计和警示信息。 -
需求分析
输入:
题目信息:包含题号、题目内容和标准答案。题号顺序不固定,允许题号缺失,题目信息可以跨行输入。
试卷信息:每张试卷包含多个题目及对应的分值,题目编号需与题目信息匹配。
学生信息:包含学号与姓名的配对,输入时统一在一行。
答卷信息:学生对试卷中各题目的回答,每道题目顺序与试卷中定义的顺序一致。
删除题目信息:表示某道题目被删除,对引用该题的试卷题目会进行“失效提示”。
输出:
试卷总分警示:当试卷分值总和不等于100分时,给出警示信息。
答卷信息:逐题输出题目内容、答案、是否正确。若题目被删除或未答题,显示对应提示。
判分信息:输出每道题目得分与总分,按试卷题目顺序,未回答或删除的题目计0分。
删除提示信息:题目被删除时,对引用该题的试卷作出“题目失效”的提示。
题目引用错误提示:试卷引用了不存在的题目,提示“non-existent question~”。
格式错误提示:若输入格式不符,直接输出错误信息。
试卷号引用错误:答卷信息中的试卷号不存在时,提示“the test paper number does not exist”。
学号引用错误:答卷信息中学号不在学生列表时,显示错误提示信息。
2. 代码结构与功能
代码采用面向对象设计,分多个类管理各类信息,主要类结构如下:
Main 类
功能:负责主程序逻辑,包括读取输入并进行初始化。
方法:
parseInput():负责解析输入数据并根据内容类型调用相应方法。
validateInput():检查输入格式,生成格式错误提示。
outputWarnings():输出试卷总分警示信息。
processAnswerSheets():处理并判定所有答卷信息。
Question 类
功能:表示单个题目,包含题号、题目内容和标准答案,并包含判定答案的方法。
方法:
isCorrectAnswer(user_answer: str) -> bool:判断用户答案是否正确。
formatForOutput(user_answer: str) -> str:格式化输出,包含题目内容、用户答案、判题结果。
TestPaper 类
功能:存储试卷信息,包含题目编号与分值,提供试卷总分警示功能。
方法:
addQuestion(question_id: int, score: int):增加试卷中的题目及对应分值。
calculateTotalScore() -> int:计算试卷总分。
outputWarningsIfNeeded():当总分不等于100分时,输出警示信息。
Student 类
功能:存储学生的基本信息。
方法:
getStudentInfo(student_id: str) -> Optional[Student]:通过学号获取学生信息。
AnswerSheet 类
功能:存储学生答卷信息,包含题目序号及对应的答案,并提供判题功能。
方法:
addAnswer(question_order: int, answer: str):记录学生答题信息。
judgeAnswers():对每个题目进行判分,并输出判题信息。
formatOutput():格式化答卷信息,包括题目内容、答案、判题结果及计分信息。
outputDeletedQuestion():生成被删除题目的提示信息。
DeletedQuestionManager 类
功能:管理被删除的题目信息。
方法:
addDeletedQuestion(question_id: int):记录被删除的题目编号。
isQuestionDeleted(question_id: int) -> bool:检查题目是否被删除。
- 代码逻辑详解
Main类逻辑
读取所有输入,通过判断前缀解析不同输入信息(如题目信息#N:、试卷信息#T:)。
逐步调用各个类的方法,将解析后的信息存储至各个类实例中。
检查试卷总分并输出警示。
对每张答卷调用processAnswerSheets()方法进行判分和格式化输出。
判题逻辑
Question类中isCorrectAnswer方法对用户答案与标准答案进行对比,返回true或false。
AnswerSheet类中调用judgeAnswers方法判定每道题的结果,依据题目是否存在、题目是否被删除等情况作不同提示。
试卷总分警示逻辑
TestPaper类的calculateTotalScore方法计算试卷总分。若总分不等于100分,则outputWarningsIfNeeded方法输出警示信息。
格式检查逻辑
Main类的validateInput方法检查输入格式,若有格式不符,输出“wrong format: +信息内容”
输出逻辑
通过AnswerSheet的formatOutput方法,逐行输出每道题目的答题信息(内容、答案、正误)。
依次输出各类提示信息:判分信息、删除题目提示、引用错误提示、格式错误提示等。
- 关键逻辑与边界条件
关键逻辑
题目与答案匹配:确保AnswerSheet类在判分时按照题目编号匹配对应答案,并输出正确格式。
删除题目:在DeletedQuestionManager中记录被删除题目,AnswerSheet判分时优先检查题目是否被删除。
引用错误提示:若试卷中题目编号不存在,优先输出“non-existent question”提示。
总分警示:TestPaper类检查试卷总分,及时输出不等于100分的警示。
边界条件
题号缺失:允许输入题号不按顺序。
空答题:答卷中缺少某题的答案时,输出“answer is null”。
不存在题目引用:当试卷引用不存在的题目时,显示“non-existent question”提示。
学号错误:答卷中学号不在学生列表时,输出学号错误提示。
5.类图
6.踩坑心得
1. 对于每部分的输入格式都必须判断是否正确,并将判断部分集合在一起,如果根据前缀分开判定,格式依然会出错
改进:在分开处理前判断格式是否正确
2. 学生ID或姓名为空:如果学生ID或姓名缺失(比如studentInfo数组长度不足),将会导致ArrayIndexOutOfBoundsException
改进:
7. 改进建议
1. 重复和冗余的代码逻辑
问题:代码中存在很多重复的逻辑,比如 input_processing 方法中对不同类型输入的处理可以抽象成一个统一的方法。
建议:提取公共代码逻辑,比如统一的字符串解析方法,将不同输入类型的处理封装为独立的方法或类。这样能减少重复代码,提高代码的复用性。
2. 错误处理不完善
问题:程序没有足够的输入验证,例如当输入格式不正确时,没有进行有效的错误提示和处理。
建议:增加输入验证和错误处理机制,确保输入格式符合要求。通过捕获异常并输出友好的错误提示来提高用户体验。
3. 学生信息的解析方式
问题:学生信息在处理时,对输入格式的假设过于严格,缺乏对空格和不规范格式的处理。
建议:在解析学生信息时,增加对输入的灵活性,使用正则表达式分割字符串以确保输入格式正确解析。
4. 方法过长
问题:input_processing 和 process 方法过长,做了太多事情,违反了单一职责原则。
建议:将长方法拆分为多个小方法,每个方法专注于完成一个单一任务。例如,将不同类型输入的解析放到独立的方法中,或者将试卷评分的逻辑抽取出来。
5. 输入解析中的魔法字符串
问题:代码中使用了很多“#N”、“#T”、“#S”等字符串,属于“魔法字符串”。
建议:将这些字符串定义为常量,方便未来修改和维护,提高代码的可读性。
改进实例:
5. 总结
通过这几次设计和实现题目的练习,我体会到了如何利用面向对象编程思维进行代码的模块化设计,同时也掌握了Java中类、对象、访问修饰符、构造器、方法等基本要素的使用。这些题目从最简单的类设计逐步过渡到更复杂的答题程序模拟,既考验了代码的规范化能力,也训练了在复杂需求下如何设计系统结构。这几次练习对Java编程的深入理解以及代码设计的能力有很大的帮助。从最初的Fan类设计到复杂的答题程序模拟,体验了面向对象编程设计方法的魅力。合理封装数据域、设置访问器与修改器、设计构造器等,是实现代码模块化的关键。不同类之间的调用、依赖关系需要充分考虑以减少代码耦合,使系统更加灵活、可扩展。复杂系统中对各个功能模块进行单独设计,并结合需求做适当的抽象和封装。比如题目、试卷和答卷信息的分别封装,使得不同功能彼此独立,方便日后扩展和维护。
通过这些作业,我对Java编程有了更深的理解。面向对象思想、代码结构设计、异常处理和测试等方面的知识得到了锻炼。在未来的学习和工作中,应该更加注重代码的可维护性、可扩展性以及容错能力,不断提升编程的效率和代码质量。