pta三次大作业总结
PTA 三次大作业总结:详细分析与实践经验
前言
这次的 PTA 大作业总结,回顾了三次作业的全面历程,涵盖了从基础到进阶的知识点、难度提升的体验,以及源码提交时的各种心情起伏。通过这三次作业,我逐步提升了自己的编程能力,从一开始的数据结构操作,到后面的复杂系统设计和管理,对面向对象的理解也变得更深入了。每一次的作业都有它独特的挑战和收获,接下来我会逐一分享其中的体会。
说实话,三次作业的难度确实是循序渐进,但每次都给我带来新的挑战。有几次,我真心感到“卡在这里头疼”。特别是在处理复杂的混合输入,还有那些涉及删除题目的设计时,真的有几次让我差点崩溃。不过,每次面对卡住的问题并成功解决后,我确实有一种“啊,这次终于搞定了!”的成就感,哪怕头发掉了几根也值了。
三次作业中的知识点层层深入,从面向对象编程、Java 集合的使用(如 List、Map),到字符串的解析,还有继承和多态——虽然多态这块我还是有些地方不太熟练,但至少比刚开始好多了。此外,还有异常处理和模块化设计,这些都让我学到了很多。比如,异常处理让我明白代码必须足够稳健,用户输入总是不可预测的,必须考虑到所有的边界情况。每次给代码加上 try-catch,感觉就像在给代码套上保护网,能让我少一点提交后再调试的焦虑。
在模块化设计方面,我开始认识到将不同功能分散到各个独立的类和方法中是多么重要。比如,把题目、答题纸、学生等信息分开管理,虽然一开始写得有些麻烦,但后来发现这样做维护起来简单得多。如果没有这些模块化的设计,我大概会被那些纠缠在一起的逻辑搞得更抓狂。
当然,整个过程中我也踩过不少坑。从字符串的解析不小心写错一个符号导致整个系统乱套,到漏考虑一些边界输入导致程序报错,这些都是“痛苦的教训”。但好在每个错误都变成了经验教训,让我对代码的细节更加敏感和谨慎。比如,现在我写字符串处理的时候,会反复考虑各种可能的输入格式,并确保用 try-catch 把可能的问题都拦住,写代码的风格也从“一气呵成”变成了“步步为营”。
但是归根到底,这三次 PTA 大作业给我带来了非常多的收获,不仅仅是在知识点上的累积,每一次的调试和改进,都是在逼迫自己变得更加细致和严谨。接下来,我希望进一步深入学习设计模式,写出更加易于扩展和维护的代码,同时也想研究更多的测试方法,以确保每段代码都能经得起各种考验。
虽然这三次作业有些时候让我抓狂,但每次完成后我也真心感受到了进步的喜悦。这种不断挑战、不断提升的过程,正是编程最吸引我的地方。
设计与分析
答题判题程序-1
任务描述
第一次大作业要求实现一个基本的答题判题系统,任务包括对题目的解析、答案的输入与判题逻辑的实现。输入包括题目数量、题目内容(题号、题目描述、标准答案)以及学生的答题信息。程序根据输入判断答案的正确性并输出结果。
类设计
1. IOHandler
类
特色代码:
String[] parts = input.split("#N:| #Q:| #A:");
int id = Integer.parseInt(parts[1].trim());
String text = parts[2].trim();
String correctAnswer = parts[3].trim();
return new QuizQuestion(id, text, correctAnswer);
分析:
这一段代码实现了通过特定的标识符 #N:
, #Q:
, #A:
来分割用户输入字符串,将其拆分成题目编号、题目内容、正确答案三个部分。使用 split("#N:| #Q:| #A:")
这样的正则表达式可以很简洁地将输入的复杂字符串分割成多个部分。随后将这些分割后的内容进行处理并创建 QuizQuestion
对象。这段代码有效地处理了复杂输入格式,是 IOHandler
类的核心内容。
2. QuizQuestion
类
特色代码:
public boolean isCorrect(String answer) {
return this.correctAnswer.equals(answer);
}
分析:
isCorrect()
方法是 QuizQuestion
类的核心逻辑,直接用来判断用户的答案是否正确。它通过使用 String
类的 equals()
方法进行比较,使得判定逻辑在 QuizQuestion
类中,而非外部进行,使每个题目自身可以判断是否回答正确,这符合面向对象的封装原则。
3. Quiz
类
特色代码:
List<QuizQuestion> questionList = new ArrayList<>(questionMap.values());
questionList.sort(Comparator.comparingInt(QuizQuestion::getId));
return questionList;
分析:
getAllQuestions()
方法中将 Map
中的题目对象转化为 List
,并且使用 Comparator.comparingInt(QuizQuestion::getId)
对题目按照题号进行排序,我认为这段代码是 Quiz
类的亮点。
new ArrayList<>(questionMap.values())
将 Map
中的值集合(题目对象)转换为 List
,方便后续操作。
Comparator.comparingInt(QuizQuestion::getId)
这段代码通过题目的 id
来进行排序,这一排序操作保证了题目按编号顺序进行排列。
4. AnswerSheet
类
特色代码:
public void evaluate() {
List<QuizQuestion> questions = quiz.getAllQuestions();
for (int i = 0; i < questions.size(); i++) {
QuizQuestion question = questions.get(i);
String userAnswer = userAnswers.get(i);
boolean isCorrect = question.isCorrect(userAnswer);
results.add(isCorrect);
}
}
分析:
evaluate()
方法是 AnswerSheet
类中我认为最具特色的部分,它使用 Quiz
中存储的所有题目与用户答案进行比对,判定每道题是否正确。
for
循环遍历每个题目,并调用 question.isCorrect(userAnswer)
这一操作将判定逻辑交给了 QuizQuestion
,让每个类都有自己的事情做。
results.add(isCorrect)
保存每一道题的判定结果。results
列表用于保存所有题目的对错判断结果,方便后续的统一输出。
5. Main
类
特色代码:
AnswerSheet answerSheet = new AnswerSheet(quiz);
answerSheet.setUserAnswers(answers);
answerSheet.evaluate();
answerSheet.showQuestionsWithAnswers();
answerSheet.showResults();
分析:
在 Main
类中,这一段代码展示了如何将各个对象组合在一起,完成整个流程:
首先,创建 AnswerSheet
对象,并将 Quiz
对象传入,使得 AnswerSheet
能够引用到所有的题目。
然后,调用 setUserAnswers()
方法将用户答案设置进去。
调用 evaluate()
方法开始判题,并使用 showQuestionsWithAnswers()
和 showResults()
输出答题的详细信息和每题是否正确的结果。
代码复杂度与质量分析(基于 SourceMonitor)
在第一次大作业中,我使用了 SourceMonitor 对代码进行了详细的复杂度分析,这里给出了项目的整体质量指标:
主要指标与结果:
代码行数:156 行
语句数:86 行
分支语句占比:8.1%
方法调用语句:40 次
注释行百分比:18.6%
类和接口数量:4
每个类的方法数量:平均 3.75
每个方法的平均语句数:5.53
最复杂的方法:Main.main()
,最大复杂度为 2
最大代码块深度:3
平均代码块深度:1.33
平均复杂度:1.12
分析图表:
从 SourceMonitor 提供的两个图表可以看出代码的复杂度和分布情况:
- 复杂度雷达图:
该图显示了代码中的复杂度分布情况,包括每个类的平均复杂度、方法数量、平均语句数、最大深度等。从图中可以看出,平均复杂度和最大复杂度都相对较低,说明代码整体上没有非常复杂的逻辑块,但方法数量和每个方法的语句数还有改进的空间。 - 块深度直方图:
深度直方图显示了代码的嵌套深度,绝大多数的代码块深度集中在 0 到 2 之间。这反映出代码逻辑结构相对扁平,避免了深度嵌套带来的复杂性,这对代码的可读性和维护性是有益的。然而,仍有一些块达到了深度 3,这些地方需要注意可能的代码复杂度。
复杂度较高的方法:
Main.main()
方法:该方法具有最大复杂度 2,语句数为 12,嵌套深度达到 3,且有 8 个方法调用。虽然整体复杂度不算高,但这是代码中最复杂的部分,说明这里逻辑较为集中。为了降低复杂度,可以考虑将一些逻辑提取成独立的方法,以减轻 main()
的职责。
代码复杂度总结
从以上的 SourceMonitor 报告中可以看出,第一次大作业的代码整体上较为简单,没有出现过于复杂的逻辑块。然而,代码的结构上存在一些可以优化的地方,特别是在注释覆盖率和模块化设计方面。
这次作业的复杂度给我带来了一些挑战,比如如何在不增加代码复杂度的前提下实现功能并保持逻辑的清晰性。之后的作业中,我将更加注重代码的可维护性,尝试减少每个方法的复杂度并提高注释的完整性。
针对这些问题,我计划在接下来的作业中,通过拆分复杂方法、增加注释覆盖率,以及使用模块化设计来进一步优化代码质量。希望通过这些改进,能提高代码的可读性和整体结构的优雅性。
说实话,第一次作业看上去很简单,但涉及到准确解析输入的时候,我还是遇到了一些麻烦。特别是输入格式稍有变化时,原来的逻辑常常崩溃,调试的过程比我预想的要痛苦得多。
类图分析(基于 PowerDesigner)
类及其结构
1. 类 IOHandler
属性:
private Scanner scanner
:用于输入的 Scanner
实例。
构造方法:
public IOHandler()
:初始化 Scanner
。
方法:
public int getQuestionCount()
:读取题目数量。
public QuizQuestion getQuestion(int index)
:读取特定题目并返回 QuizQuestion
对象。
public List<String> getUserAnswers()
:读取用户的答案。
2. 类 QuizQuestion
属性:
private int id
:题目编号。
private String text
:题目内容。
private String correctAnswer
:正确答案。
构造方法:
public QuizQuestion(int id, String text, String correctAnswer)
:初始化题目对象。
方法:
public int getId()
:返回题目编号。
public String getText()
:返回题目内容。
public String getCorrectAnswer()
:返回正确答案。
public boolean isCorrect(String answer)
:检查用户答案是否正确。
3. 类 Quiz
属性:
private Map<Integer, QuizQuestion> questionMap
:存储题目的映射。
构造方法:
public Quiz()
:初始化题目映射。
方法:
public void addQuestion(QuizQuestion question)
:添加题目到题目列表。
public List<QuizQuestion> getAllQuestions()
:获取所有题目,按编号排序返回。
4. 类 AnswerSheet
属性:
private Quiz quiz
:关联的 Quiz
对象。
private List<String> userAnswers
:用户答案列表。
private List<Boolean> results
:判题结果列表。
构造方法:
public AnswerSheet(Quiz quiz)
:初始化答题卡,关联指定的 Quiz
。
方法:
public void setUserAnswers(List<String> answers)
:设置用户输入的答案。
public void evaluate()
:判题,根据用户答案与正确答案对比并存储结果。
public void showQuestionsWithAnswers()
:显示题目及用户的答案。
public void showResults()
:显示判题结果(正确或错误)。
关系示意
依赖关系:
从 IOHandler
到 QuizQuestion
以及从 AnswerSheet
到 Quiz
使用 虚线 表示依赖。
聚合关系:
从 Quiz
到 QuizQuestion
使用 空心菱形 表示聚合关系。
从 AnswerSheet
到 Quiz
也使用 空心菱形 表示聚合关系。
设计心得
通过这次作业,我学会了如何将复杂的输入解析并映射到对象结构中,借助集合框架高效管理数据。正则表达式用于解析不确定格式的输入,为后续复杂系统开发打下了基础。面向对象编程的封装性让数据结构的管理更加简洁和直观,也为后续的功能扩展提供了很好的基础。
答题判题程序-2
任务描述
第二次大作业要求实现包含试卷管理的判题系统。引入了试卷的概念,用户可以为试卷指定题目和分值,系统需要根据这些信息判定学生的答题情况并计算总分。
类设计
1. Question
类
特色代码:
public Question(int number, String content, String answer) {
this.number = number;
this.content = content;
this.answer = answer;
}
分析:
这构造器没啥可讲的,就是一个基础模板。但也别小看它,这里封装了题号、题干和答案,每个对象都齐全地记录下来。我可不想未来再弄一堆散乱的数据。老老实实给每个题目三条属性,妥妥地把它们绑在一起,这种简单粗暴的方式反而最让我安心。
2. TestPaper
类
特色代码:
public void addQuestion(int questionNumber, int score) {
this.questions.add(new QuestionItem(questionNumber, score));
this.totalScore += score;
}
分析:
每次添加题目就直接加分,这种“多此一举”其实是为了防止未来出现不必要的麻烦。我可不想最后发现总分少算了几道题,再满世界找 bug,所以每次加题目就加分,省得晚点再去算得晕头转向。
3. QuizEvaluation
类
特色代码1:parseInput()
方法
String[] parts = line.split("#N:")[1].split("#Q:");
int questionNumber = Integer.parseInt(parts[0].trim());
String[] contentAndAnswer = parts[1].split("#A:");
分析:
这段 split()
的操作让我不得不回忆起那些日子里为了处理输入数据头痛的场景。最早写的版本用一堆 if
,搞得异常复杂,每次出错我都想摔键盘。后来干脆来个连续 split()
,虽然看起来复杂,但实际比那堆 if
好太多了!现在代码结构清晰多了,读起来也顺眼,虽然折腾了不少次,总算找到了这种最省心的方式。
特色代码2:evaluate()
方法
if (paper.totalScore != 100) {
System.out.println("alert: full score of test paper" + paper.number + " is not 100 points");
}
分析:
检查总分这部分是给未来的我留的提醒。虽然看似有点“强迫症”,但我真不希望哪天别人跟我说:“这套试卷居然不是 100 分?”这种提醒就像是在代码里给自己留个小防线,万一有个差错,至少它会大声提醒我:“嘿,这里出问题了!”多一步提醒,少几分头痛。
特色代码3:回答与判定
if (givenAnswer.equals(correctAnswer)) {
scores.add(item.score);
System.out.println(questions.get(item.questionNumber).content + "~" + givenAnswer + "~true");
} else {
scores.add(0);
System.out.println(questions.get(item.questionNumber).content + "~" + givenAnswer + "~false");
}
分析:
回答对比这段我想了很久,是不是该用更复杂的比较方式?但想了想,老老实实用 equals()
最合适,毕竟直接有效。每次判断完答案,输出 true
或 false
,这真的是最“直接了当”的方式了,想复杂不如直接说“对还是错”。虽然“土”,但好调试,谁用谁知道。
4. AnswerSheet
类
特色代码:
public AnswerSheet(int paperNumber, List<String> answers) {
this.paperNumber = paperNumber;
this.answers = answers;
}
分析:
这个构造器写得太舒服了,简单、清晰,正好是“搭建”答题纸的样子。说实话,在被 split()
折磨了那么久之后,终于有这样一个不用想复杂的类,真是舒坦。毕竟不是什么时候都得往死里优化,有些地方简单点反而让整个结构看起来更清爽。
5. Main
类
特色代码:
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
inputLines.add(line);
if (line.trim().equals("end")) {
break;
}
}
无限 while
循环,直到输入 "end"
,其实我挺相信输入者不会乱来的。要是真的有人恶搞,我也没辙。
这段代码有时候让我吐槽得不行,但最终还是有种成就感。它们一起让整个答题系统稳稳地工作,也让我学会了在细节和简洁之间找到平衡。代码不总是完美,但每一步努力都在让它变得更好一点。
代码复杂度与质量分析(基于 SourceMonitor)
在第二次大作业中,我再次使用了 SourceMonitor 对代码进行详细的复杂度分析,以下是主要的质量指标和代码分析结果:
主要指标与结果:
代码行数:145 行
语句数:100 条
分支语句占比:18.0%
方法调用语句:51 次
注释行百分比:0.0%
类和接口数量:6
每个类的方法数量:平均 1.33
每个方法的平均语句数:9.00
最复杂的方法:QuizEvaluation.evaluate()
,最大复杂度为 10
最大代码块深度:6
平均代码块深度:2.81
平均复杂度:3.25
分析图表:
-
复杂度雷达图(Kiviat 图):
从雷达图中可以看出,代码在复杂度方面有了显著增加,特别是最大复杂度和代码块深度部分较为突出。平均复杂度为 3.25,说明代码逻辑开始复杂化,特别是在QuizEvaluation.evaluate()
方法中,其复杂度达到了 10,使得代码不太好维护和阅读。
方法数和每个方法的语句数有所减少,但平均语句数量增多,表明代码中有些方法承担了较多的逻辑处理任务。 -
块深度直方图:
块深度分布显示,嵌套深度达到了 6,其中嵌套深度为 4 和 5 的部分占比较高。深度过大的代码块会增加阅读和理解的难度,也提高了潜在的维护成本。
代码中较为复杂的逻辑部分集中在块深度为 4 到 6 的范围内,这表明代码的条件逻辑和流程控制相对集中在某些特定模块,特别是在QuizEvaluation
类中。
复杂度较高的方法:
QuizEvaluation.evaluate()
方法:这是代码中最复杂的部分,其复杂度为 10,语句数为 26,最大块深度为 6,同时包含 19 个方法调用。这些指标表明 evaluate()
方法逻辑非常复杂,包含了多个嵌套条件和分支处理。
将来对此代码的改进想法:
- 提高注释覆盖率:目前的注释覆盖率为 0%,但是其实我的代码里面是有注释的但是不知道为什么导入SourceMonitor里面他就说我注释百分比为零,但是也让我认识到注释也是十分主要的,尤其是考虑到
QuizEvaluation.evaluate()
的复杂性,增加详细的注释可以帮助自己在过去一段时间仍然可以读懂自己的代码,特别是在复杂的条件处理部分。 - 优化复杂方法:对
QuizEvaluation.evaluate()
进行重构,将逻辑分支独立为不同的辅助方法,减少该方法的复杂度。这将有助于降低代码的耦合度,提高可读性。 - 减少嵌套深度:块深度为 4 到 6 的代码块较多,复杂的嵌套逻辑使得代码难以理解。可以考虑简化条件判断或者将部分逻辑抽取为独立的方法,从而降低嵌套深度。
- 模块化处理逻辑:在当前的类设计中,有些类的职责过多,例如
QuizEvaluation
类。通过将复杂的逻辑分布到更多小型且职责单一的类中,代码会更加模块化,也更加符合单一职责原则。
代码复杂度总结
从以上的 SourceMonitor 报告可以看出,第二次作业的代码复杂度显著增加,特别是由于试卷管理和答案评价功能的引入,增加了代码中的分支和条件处理逻辑。注释的缺乏使得这些复杂部分的可读性较低。面对这些挑战,我意识到在应对复杂需求时,必须更加注重代码的模块化设计和注释,以提升代码的可维护性和可读性。
示例更新后的总结片段
“在第二次作业中,SourceMonitor 的分析结果显示代码的复杂度有了显著提升,特别是在 QuizEvaluation.evaluate()
方法中,其复杂度达到 10,块深度为 6。这些数据表明,该方法在承担较多复杂逻辑的同时,缺乏清晰的模块化结构。结合 SourceMonitor 生成的图表,我们可以清楚看到代码中的嵌套深度和复杂度主要集中在几个特定的方法内,特别是评估和解析输入的部分。
在接下来的改进中,我将重点放在复杂逻辑的拆分和模块化设计上,同时增加代码注释,帮助未来的维护者更好地理解和扩展系统。通过这些改进,我希望代码可以在复杂性上得到有效的控制,同时提高开发过程中的协作性和易用性。”
在这次作业中,最大的难点莫过于混合输入的处理。有时候,感觉自己快被这些乱七八糟的输入搞得头大。但好在,通过多轮遍历的方法,对不同类型的数据进行分类存储,最终还是顺利解决了问题。
类图分析(基于 PowerDesigner)
类图详细构成
1. 类 Question
属性:
number: int
:问题编号,表示问题的唯一标识符。
content: String
:问题内容,表示具体的题目文本。
answer: String
:问题答案,表示正确的回答。
构造方法:
Question(int number, String content, String answer)
:构造函数,用于初始化问题对象。
2. 类 TestPaper
属性:
number: int
:试卷编号,唯一标识试卷。
questions: List<QuestionItem>
:包含多个 QuestionItem
的列表,表示试卷中的问题项。
totalScore: int
:试卷的总分,表示所有问题的分数总和。
构造方法:
TestPaper(int number)
:构造函数,用于初始化试卷对象。
方法:
addQuestion(int questionNumber, int score)
:添加问题项到试卷,并更新总分。
内部类 QuestionItem
:
属性:
questionNumber: int
:该问题的编号。
score: int
:该问题的分数。
构造方法:
QuestionItem(int questionNumber, int score)
:构造函数,用于初始化问题项对象。
3. 类 AnswerSheet
属性:
paperNumber: int
:试卷编号,表示答题卡对应的试卷。
answers: List<String>
:答案列表,存储学生对每个问题的回答。
构造方法:
AnswerSheet(int paperNumber, List<String> answers)
:构造函数,用于初始化答题卡对象。
4. 类 QuizEvaluation
静态属性:
questions: Map<Integer, Question>
:存储所有问题的映射,按问题编号索引。
testPapers: Map<Integer, TestPaper>
:存储所有试卷的映射,按试卷编号索引。
answerSheets: List<AnswerSheet>
:存储所有答题卡的列表。
静态方法:
parseInput(List<String> inputLines)
:解析输入行,创建问题、试卷和答题卡。
evaluate()
:评估试卷和答题卡,输出结果。
类之间的关系
-
组合关系:
TestPaper
和QuestionItem
:
TestPaper
类拥有多个QuestionItem
,这是一个组合关系。 -
聚合关系:
QuizEvaluation
和其集合:
QuizEvaluation
类聚合多个Question
、TestPaper
和AnswerSheet
。 -
依赖关系:
QuizEvaluation
依赖于Question
和TestPaper
:
设计心得
第二次作业的挑战在于处理混合输入和多数据类型的管理。我使用 Map
存储题目信息,List
存储试卷题目和分值,实现了高效检索。通过对不同数据结构的合理使用,我确保了系统的高效性和数据一致性。这次作业也让我学会了如何更好地将实体间的关系映射到类设计中,使得代码更具可读性和扩展性。
答题判题程序-3
任务描述
第三次大作业在已有基础上增加了学生管理和题目删除功能,系统需要管理学生信息,支持删除题目,引用已删除题目的试卷仍需保持有效,但该题目记为无效,得分为零。
类设计
1. Question
类
特色代码:
public Question(int number, String content, String answer) {
this.number = number;
this.content = content;
this.answer = answer;
}
分析:
这构造是无数个小“砖块”的代表——每道题就是一块砖,整个系统就是一面墙。我可不想这面墙里有一块不完整的砖,所以题号、内容、答案全都封好,一个也不能少。虽然写起来很普通,但这就是必要的。
2. TestPaper
类
特色代码:
public void addQuestion(int questionNumber, int score) {
this.questions.add(new QuestionItem(questionNumber, score));
this.totalScore += score;
}
分析:
这里我觉得还是多加个分数累加操作比较好,每次加题目就顺带把分数加了,省得等到最后才去计算一大堆。说实话,这样写就算多了一点小工作量,但能避免自己到处出错找 bug,那真的值了。毕竟,一次出错就得折腾半天,我真不想再浪费调试时间。
3. AnswerSheet
类
特色代码:
public void addAnswer(int questionOrder, String answer) {
this.answers.put(questionOrder, answer.trim());
}
分析:
老实说,这里小小的 trim()
是个东西,尤其是可能会不小心多输入空格的时候。我可不想用户因为不小心加了个空格导致答案判错,这简直是代码界的“陷阱”。这小小一行代码拯救了无数次心态崩溃的情况,看到它我都忍不住给自己点个赞。
4. Student
类
特色代码:
public Student(String id, String name) {
this.id = id;
this.name = name;
}
分析:
这构造器简单到没什么可说的——只是一张学生的身份证明罢了。我特意用 id
和 name
记录学生信息,这样一旦有人交了答题纸,我能立刻知道谁在考试。没花哨逻辑,但也正因为它简单,后续操作也都变得简单——谁还会不喜欢这样的代码呢?
5. QuizEvaluation
类
特色代码1:parseInput()
方法
try {
if (line.startsWith("#N:")) {
parseQuestion(line);
} else if (line.startsWith("#T:")) {
parseTestPaper(line);
} else if (line.startsWith("#X:")) {
parseStudents(line);
} else if (line.startsWith("#S:")) {
parseAnswerSheet(line);
} else if (line.startsWith("#D:N-")) {
parseDeletedQuestion(line);
} else if (line.trim().equals("end")) {
break;
} else {
System.out.println("wrong format:" + line);
}
} catch (Exception e) {
System.out.println("wrong format:" + line);
}
分析:
parseInput()
这里其实就是个万能解析器,用 try-catch
来应对各种输入可能出错的情况。真的是“宁可错杀一千,不可放过一个”。毕竟用户输入什么都得考虑,写这段的时候我觉得自己仿佛是个保险公司,防止一切事故发生——虽然折腾得不轻,但之后不用担心乱七八糟的输入搞垮整个程序。
特色代码2:evaluateAnswers()
方法
for (int i = 0; i < paper.questions.size(); i++) {
TestPaper.QuestionItem item = paper.questions.get(i);
String givenAnswer = sheet.answers.getOrDefault(i + 1, "");
if (givenAnswer.isEmpty()) {
System.out.println("answer is null");
} else if (!questions.containsKey(item.questionNumber)) {
System.out.println("non-existent question~0");
} else if (deletedQuestions.contains(item.questionNumber)) {
System.out.println("the question " + item.questionNumber + " invalid~0");
} else {
Question question = questions.get(item.questionNumber);
if (givenAnswer.equals(question.answer)) {
System.out.println(question.content + "~" + givenAnswer + "~true");
} else {
System.out.println(question.content + "~" + givenAnswer + "~false");
}
}
}
分析:
答案判定部分写得非常“保守”。各种边界情况全都考虑到了,未回答、题目不存在、题目被删除……每次输出 true
或 false
,这很直接。写到这里时,我几次想要缩短代码,但后来发现,“对和错”的直接对比才是最不容易出错的。虽然有时候觉得写这么多判断有点笨,但能少找 bug,这笨我心甘情愿。
特色代码3:删除题目的解析
private static void parseDeletedQuestion(String line) {
int questionNumber = Integer.parseInt(line.split("#D:N-")[1].trim());
deletedQuestions.add(questionNumber);
}
分析:
删除题目这一块加上去的时候,我心里想着:天呐,能别再加功能了吗?但为了让系统更加灵活,只好硬着头皮搞定它。加个 Set
去存被删的题目号,这样后面检查时就能省心一些。虽然感觉有点“多此一举”,但长远来看,这步还是有必要的,毕竟考试里谁知道什么时候题目得“作废”呢?
代码复杂度与质量分析(基于 SourceMonitor)
在第三次大作业中,我继续使用了 SourceMonitor 对代码进行了详细的复杂度分析,以下是主要的质量指标和代码分析结果:
主要指标与结果:
代码行数:242 行
语句数:163 条
分支语句占比:19.6%
方法调用语句:91 次
注释行百分比:0.0%
类和接口数量:7
每个类的方法数量:平均 2.71
每个方法的平均语句数:6.21
最复杂的方法:QuizEvaluation.parseInput()
,最大复杂度为 10
最大代码块深度:5
平均代码块深度:2.38
平均复杂度:2.84
分析图表:
-
复杂度雷达图(Kiviat 图):
图中显示,代码的复杂度有一定的上升,尤其是平均复杂度和最大复杂度部分。QuizEvaluation.parseInput()
方法的复杂度达到 10,这意味着其包含了多个条件和分支逻辑,使得代码结构较为复杂,理解和维护都相对困难。
每个类的方法数量相比第二次作业有所增加,平均每个方法的语句数为 6.21。尽管这一数值尚处于可接受的范围内,但也意味着一些方法在承担更多逻辑任务,需要关注其是否符合单一职责原则。 -
块深度直方图:
块深度分布显示,嵌套深度达到了 5,大部分代码块的深度集中在 2 到 3 之间。这一分布表明代码逻辑存在一定程度的复杂嵌套,特别是在QuizEvaluation
类中。这种深度的代码块使得维护成本增高,并增加了代码中的潜在 bug 风险。
深度为 5 的部分仍然存在不小的比例,应考虑简化这些逻辑,通过将部分代码抽象为独立的方法来减少复杂度。
复杂度较高的方法:
QuizEvaluation.parseInput()
方法:这是代码中最复杂的方法,其复杂度为 10,包含 17 条语句,最大块深度为 5,同时包含 14 个方法调用。这些指标说明 parseInput()
方法逻辑极为复杂,可能涉及多重解析和处理流程。建议将其中复杂的逻辑提取为多个小型方法,从而简化流程并提高代码的可读性。
QuizEvaluation.evaluateStudentScore()
方法:复杂度为 9,语句数为 16,块深度为 5,且包含 18 个方法调用。这种复杂性意味着该方法内包含了多个嵌套和条件判断,增加了理解的难度。
将来对此代码的改进想法:
- 提高注释覆盖率:第三次作业的注释覆盖率导出的仍然是 0%,但是其实我的代码里面是有注释的但是不知道为什么导入SourceMonitor里面他就说我注释百分比为零,但是也让我认识到注释也是十分主要的,尤其是考虑到
QuizEvaluation.parseInput()
和QuizEvaluation.evaluateStudentScore()
这些复杂方法中。增加详细注释可以极大帮助未来的开发者理解代码的意图和逻辑流。 - 重构复杂方法:特别是
QuizEvaluation.parseInput()
和evaluateStudentScore()
,建议将这些方法中复杂的条件判断提取到辅助方法中。这不仅能降低每个方法的复杂度,还能提高代码的复用性。 - 减少嵌套深度:嵌套深度达到 5 的部分应引起重视。可以考虑通过简化条件逻辑,使用策略模式或者通过抽象类和接口来减少代码块的嵌套层次。这样可以减少代码的复杂度,提高代码的清晰性。
- 模块化处理输入逻辑:当前
parseInput()
方法负责了太多的输入解析和处理工作,通过将不同的输入类型分配到各自的专用方法或类中,代码会更加简洁,职责也更加单一化。
代码复杂度总结
从以上的 SourceMonitor 报告可以看出,第三次大作业的复杂度较之前进一步增加,特别是在 QuizEvaluation
类的输入解析和学生分数评估部分。这些复杂部分使得代码的可维护性降低,也在潜在的扩展中存在较大挑战。面对这些问题,增加注释、降低单个方法的复杂度、减少嵌套深度、模块化复杂逻辑是必要的改进方向。
示例更新后的总结片段
“在第三次作业中,SourceMonitor 分析的结果显示,代码的复杂度进一步上升,尤其是在 QuizEvaluation
类的几个核心方法中,如 parseInput()
和 evaluateStudentScore()
。这些方法的复杂度分别达到了 10 和 9,块深度也高达 5。这些指标说明,代码在处理输入解析和学生成绩评估时承担了过多的逻辑,导致维护和扩展变得非常困难。
针对这些复杂性,未来我计划通过以下几个方向进行改进:一是增加注释,让每个逻辑部分更加清晰易懂;二是将复杂的方法拆分成更小的子方法,使得代码逻辑更为简单;三是降低嵌套深度,通过更加简化的流程控制或分解职责的方式来降低维护难度。这些改进将帮助代码保持更高的清晰度和可维护性,为未来的开发人员减少阅读和理解代码的难度。”
希望这些内容能帮助您在总结报告中更好地反映第三次大作业的代码质量与改进方向。如果有其他具体需求或需要进一步补充的部分,请告诉我。
说实话,第三次作业的复杂性让我一度怀疑人生。特别是在处理题目删除后的引用管理时,如何让系统稳定地处理那些已经被删除的引用是一件非常头疼的事。不过,当最终实现了这一功能,并且看到系统能够正确输出时,那种成就感也是相当不错的。
类图分析(基于 PowerDesigner)
1. Question
类
属性:
int number
:问题编号
String content
:问题内容
String answer
:问题的正确答案
构造方法:
Question(int number, String content, String answer)
:初始化问题编号、内容和答案
2. TestPaper
类
属性:
int number
:试卷编号
List<QuestionItem> questions
:试卷中的问题项列表(QuestionItem
是内部类)
int totalScore
:试卷的总分
方法:
addQuestion(int questionNumber, int score)
:添加一个问题项(包括问题编号和分数),并更新试卷总分
内部类:QuestionItem
属性:
int questionNumber
:问题编号
int score
:该问题的分数
构造方法:
QuestionItem(int questionNumber, int score)
:初始化问题编号和分数
3. AnswerSheet
类
属性:
int paperNumber
:试卷编号(对应的试卷)
String studentId
:学生 ID
Map<Integer, String> answers
:存储问题序号和对应的学生答案的映射
构造方法:
AnswerSheet(int paperNumber, String studentId)
:初始化试卷编号和学生 ID,并创建空的答案映射
方法:
addAnswer(int questionOrder, String answer)
:添加学生对某个问题的答案
4. Student
类
属性:
String id
:学生 ID
String name
:学生姓名
构造方法:
Student(String id, String name)
:初始化学生 ID 和姓名
5. QuizEvaluation
类
属性:
Map<Integer, Question> questions
:存储所有 Question
对象的集合,按问题编号索引
Map<Integer, TestPaper> testPapers
:存储所有 TestPaper
对象的集合,按试卷编号索引
Map<String, Student> students
:存储所有 Student
对象的集合,按学生 ID 索引
List<AnswerSheet> answerSheets
:存储所有学生的答题卡 AnswerSheet
Set<Integer> deletedQuestions
:存储被删除的问题编号
方法:
parseInput(List<String> inputLines)
:解析输入数据,根据行的标记选择合适的解析方法
evaluate()
:调用 evaluateTestPapers()
和 evaluateAnswerSheets()
进行评估
私有解析方法:
parseQuestion(String line)
:解析问题行
parseTestPaper(String line)
:解析试卷行
parseStudents(String line)
:解析学生信息行
parseAnswerSheet(String line)
:解析答题卡行
parseDeletedQuestion(String line)
:解析被删除的问题行
私有评估方法:
evaluateTestPapers()
:检查每份试卷的总分是否为 100 分
evaluateAnswerSheets()
:评估所有学生的答题卡
evaluateAnswers(AnswerSheet sheet, TestPaper paper)
:评估答题卡中的每个答案是否正确
evaluateStudentScore(AnswerSheet sheet, TestPaper paper)
:计算学生的总得分并输出结果
设计心得
本次作业的难点在于如何管理被删除题目的引用。我通过在 TestPaper
中增加引用检查,在输出时标记无效题目,确保了系统的稳定性。此外,使用异常处理机制捕获输入错误增强了代码健壮性。在处理删除逻辑时,我学会了如何应对引用丢失的情况,以及如何在保证程序正常运行的同时正确提示用户。
采坑心得
-
正则表达式解析问题:第一次作业中,由于输入格式多样,正则表达式多次匹配失败。例如在解析某些特殊字符时,由于正则表达式的写法不够灵活,导致程序崩溃。经过反复调试和查阅资料,最终找到了一种适合所有情况的解析规则,使得代码能够稳定处理不同的输入格式。
-
混合输入顺序管理:第二次作业需要处理复杂的混合输入,不同的数据类型需要按顺序进行处理,而最初的方案没能有效处理顺序,导致测试失败。后来我引入了多轮遍历的方法,将数据分为不同的类型进行多次遍历,这样不仅保持了数据的顺序,也大大提高了数据分类的效率。
-
引用已删除题目的处理:第三次作业中新增了题目删除功能,而引用已删除题目的情况最初导致了程序崩溃,因为系统在查找已经删除的题目时无法处理空引用。我增加了一套引用检查机制,在题目删除后,将其在试卷和答题记录中的状态标记为“无效”,在后续的评分中输出“此题已删除,不计分”。
-
对于答案为空的情况处理:第三次作业相比与前面的作业难度可谓是指数增长,尤其是这一个测试点,我花费了很长的时间一直在这个测试点上,就是输入的答案字符为空例如“ ”,也算没有答案,我想着可以在
if (givenAnswer.isEmpty()) {
System.out.println("answer is null");
} else if (!questions.containsKey(item.questionNumber)) {
System.out.println("non-existent question~0");
} else if (deletedQuestions.contains(item.questionNumber)) {
System.out.println("the question " + item.questionNumber + " invalid~0");
} else {
Question question = questions.get(item.questionNumber);
if (givenAnswer.equals(question.answer)) {
System.out.println(question.content + "~" + givenAnswer + "~true");
} else {
System.out.println(question.content + "~" + givenAnswer + "~false");
}
}
这里的判断中进行优化,但是始终达不到效果,一直到截止日期的时候都没有解决,也造就了这一次大作业有三个测试点过不去,没能拿下满分的遗憾。
改进建议
-
代码模块化与解耦:在前三次作业中,判题逻辑与数据解析高度耦合,建议将判题与解析完全独立,以提高复用性和可维护性。比如,可以将输入解析部分封装成一个独立模块,专门负责处理数据的格式化和转换。
-
输入验证的严格性:建议在输入阶段加强验证,避免特殊字符或错误格式导致的异常。尤其是第三次作业中,我因未严格验证输入而遇到多次失败。比如在处理学生答题信息时,如果能在输入阶段提前对非法字符进行过滤,将会极大减少后续判题的错误率。
-
数据结构的选择优化:面对大规模数据时,可以考虑
TreeMap
或HashSet
来提高数据管理的效率。例如,TreeMap
可以在需要有序访问题目或学生信息时提供更高的效率,而HashSet
则能在判重和快速查找时发挥很大作用。
总结:在 PTA 大作业中学到的内容
-
面向对象编程的深入理解
通过设计Question
、TestPaper
、AnswerSheet
、Student
等类,我加深了对面向对象编程的理解。
封装:将不同的数据封装到类中,比如Question
把题号、题干和答案封装到一起,这让我意识到如何设计好一个类,使它具备明确的单一职责。
继承与组合:虽然这次没有用到继承,但使用了组合来构建复杂对象,比如在TestPaper
中将QuestionItem
作为内部类,这种方式帮助我更好地组织相关数据。 -
字符串处理和输入解析
通过parseInput()
方法,我对字符串的处理变得更加熟练。学会了使用正则表达式、split()
等方法来处理复杂的输入格式。
学到的一个重要点是,用户输入的各种不可预知性(格式错误、遗漏信息)需要额外的错误处理机制,比如 try-catch 来捕获异常。这让我意识到,编写稳健的代码不仅仅是逻辑的正确性,还包括对用户输入和异常情况的处理。 -
复杂逻辑的解耦和职责分离
在QuizEvaluation
中,解析不同的数据(如题目、答题纸、学生信息)分别使用独立的方法,比如parseQuestion()
、parseTestPaper()
等。
学会了如何将逻辑划分到不同的方法和类中,使每个类和每个方法都有明确的职责。这样能让代码变得更清晰,还能减少一些奇奇怪怪的bug。 -
数据结构的选择和运用
学会了如何选择合适的数据结构来存储信息。比如:
用Map<Integer, Question>
存储题目,通过题目编号可以快速查找。
用List<AnswerSheet>
存储所有答题纸,便于在后续评判中逐一遍历。
用Set<Integer>
存储删除的题目编号,用于快速查找已被删除的题目。
数据结构的合理选择是影响代码效率和可扩展性的关键,这次大作业让我体会到各类数据结构的优劣及适用场景。 -
调试与错误处理
写代码的过程中,我对错误处理有了更多的体会。比如,在字符串解析中容易因为格式错误而出现ArrayIndexOutOfBoundsException
,为了解决这个问题,我尝试了不同的错误处理方式,最终选择了在parseInput()
中加上try-catch
块。
学会了用简单明了的提示信息告知用户错误的原因,比如输入格式不正确,这样可以提升程序的用户友好性,同时也方便自己调试。
需要进一步学习和研究的地方
-
更优雅的字符串处理
尽管使用了split()
和一些正则表达式来分割输入字符串,但代码看起来还是比较复杂且容易出错。
需要进一步学习正则表达式的高级用法,以及其他更优雅的解析方式,比如使用状态机或者第三方库来解析输入数据,以减少对字符串处理的依赖和出错的概率。 -
代码的可维护性与可读性
有些方法过于长,例如parseInput()
,包含了对所有输入的解析逻辑,这会导致代码可读性降低。
需要进一步学习如何提升代码的可维护性,比如采用更加清晰的方法名、合理的注释以及减少大方法。
最终反思
简单与复杂的平衡:整个项目让我意识到,简单的代码不等于容易实现。有些时候,为了使代码在未来更容易维护,需要更多的思考和设计。而代码越简单,实际上隐含了更多对可能错误的防范。
扩展性与灵活性:如何设计一个灵活的系统,是这次作业给我的最大启发之一。增加删除题目的功能让我意识到,在最初设计时就考虑系统的扩展性是多么重要的一件事。写代码不仅要为现在,还要为未来。
时间管理与调试:调试花费的时间远远超过了写代码的时间。这让我意识到,代码一次写对的概率很低,因此在写代码时应该多考虑可能的错误路径,并提前做好防范。
三次大作业下来,我的编程水平有了显著提高,从单纯写出代码到注重代码的结构和设计,我还发现了许多可以改进的地方。这让我对接下来的学习充满期待和动力,继续把每一次编码都当作成长的机会。
对课程与作业的建议
理论与实践结合:建议课程增加更多关于代码优化、设计模式和性能分析的内容,帮助学生更好地将理论与实践结合。
代码审查与合作:增加代码审查环节,鼓励学生间互相审查代码,交流心得,从中学习不同的设计思路和实现方法。
更多案例分析:希望课程中能增加对实际开发中常见问题的案例分析,帮助学生更好地理解如何解决复杂问题。
PTA 的三次大作业充满挑战,但也为我带来了非常宝贵的学习和成长经历。感谢老师和同学们在此过程中的帮助和支持,这些收获为我未来的学习和工作打下了坚实基础。