oop-first-blog
1.前言
1.一些碎碎念
这三次作业在我看来都是比较具有挑战性的,以题促学,为了写出这三道题目,我们需要去学习更多java中独特的语法和基本结构,同时这也加深了我对java语言中的基础知识的理解和掌握,做得到了从“会听”到“会用”的进步。令我受益匪浅,也掌握了一些对字符串的基本处理操作,这是我以前学c语言时未曾深入研究学习到的。
2.分析
1.第一次题目集
第一次题目集的最后一题难度为中等,主要设计到类的设计、定义类和对象,封装数据和行为、泛型的使用、字符串的处理以及基本的控制结构,难点主要集中在数据输入的处理上,不能简单的根据空格和字符进行split分割,比如对题目或者答案中空格的处理,还有题目和答案不按照格式输入的处理,多余空格的处理,这一列问题都需要我去研究字符串的处理操作进行处理。
### 2.第二次题目集
第二次题目集的最后一题难度就开始增加了,在一开始的基础上增加了试卷和答卷,这样来看至少增加了两个类,毫无疑问难度上升了不少,因为有试卷的存在,从一开始的仅仅判断题目的正误变成了根据测试卷寻找题目以及根据答卷验证题目正确性的操作,这又对我们提出了不少的要求,比如题目在测试卷类中的存储方式,以及测试卷在题目列表中找到题目的方式,需要采用合适的方式对题目进行储存的处理,便于后续两个类对其的访问和处理,需要使用数组或集合(如ArrayList)存储题目和答案,对异常的处理:使用try-catch块处理可能出现的异常,map的使用和相关操作。
### 3.第三次题目集
第三次题目集的最后一题增加的东西又更多了,首先它完善了答卷的信息,答题人的学号姓名,毫无疑问,这需要我们再次新建一个学生类,其次它又添加了新操作对于某道题的删除处理(暂不考虑被删除题目不存在的状况),这可能需要我们遍历题目列表进行相应的操作,这些信息添加使得用户端输入的文本变得更加庞大,我需要用到字符串相关的操作例如正则表达式对文本进行相应的处理,因为题目信息删除的引入和答卷内容的完善,需要处理异常输入情况变得更多,我需要使用更多的异常用例进行测试和封装部分代码专门处理异常情况的输入,对于空格的输入这类数据的处理要求变得更加严格。
2.设计与分析
## 1.一次设计
复杂度:
Complexity metrics | 周日 | 21 4月 2024 22:06:51 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
Main.main(String[]) | 17 | 1 | 9 | 9 |
question.question(String, String, String) | 0 | 1 | 1 | 1 |
question.suit(String) | 0 | 1 | 1 | 1 |
Class | OCavg | OCmax | WMC | |
Main | 9 | 9 | 9 | |
question | 1 | 1 | 2 | |
Package | v(G)avg | v(G)tot | ||
3.67 | 11 | |||
Module | v(G)avg | v(G)tot | ||
untitled6 | 3.67 | 11 | ||
Project | v(G)avg | v(G)tot | ||
project | 3.67 | 11 |
public class Main {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
int n;
String num,ques,answer;
String line;
String []part = new String[6];
n= input.nextInt();
input.nextLine();
question []q1=new question[n];
for(int i=0;i<n;i++)
{
line=input.nextLine();
part=line.split("#[A-Z]:| ");
q1[i]=new question(part[1],part[5],part[3]);
}
String []userAnswer=new String[2*n];
line= input.nextLine();
userAnswer=line.split("#[A-Z]:| ");
int k=1;
int number;
int flag=1;
for(int j=0;j<n;j++)
{
for(int o=0;o<n;o++)
{
number=Integer.parseInt(q1[o].num);
if(number==flag)
{
System.out.printf("%s~%s\n",q1[o].quest,userAnswer[k]);
}
}
k+=2;
flag++;
}
k=1;
flag=1;
for(int e=0;e<n;e++,k+=2)
{
for(int o=0;o<n;o++)
{
number=Integer.parseInt(q1[o].num);
if(number==flag)
{
System.out.printf("%s",q1[o].suit(userAnswer[k]));
if(e!=n-1)
{
System.out.printf(" ");
}
}
}
flag++;
}
}
}
解释:因为第一次设计到的类少,故只设计了一个question类来进行题目属性和行为的封装操作,main类为测试类,负责对输入的文本进行处理读入数据的操作,question类中含判断题目正确与否的操作,输出的操作也放置在main类中,main类读取信息来创建question并且存储在数组中。main中第一个for循环进行数据的读取和new question的创建,而第二个for循环则对用户的答案进行处理,最后一个循环用来输出对应题目相应的结果。
心得:即使题目内容比较少,我创建的类似乎过于简单了,main类的操作显得有些冗余,可以再创建一个类进行细化。
2.二次设计
复杂度:
Complexity metrics | 周日 | 21 4月 2024 22:06:12 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
AnswerSheet.AnswerSheet(String, String) | 0 | 1 | 1 | 1 |
AnswerSheet.answerCheck(String, String) | 0 | 1 | 1 | 1 |
AnswerSheet.mapPrint() | 1 | 1 | 2 | 2 |
AnswerSheet.resultOfAnswerSheet(List |
31 | 10 | 13 | 13 |
AnswerSheet.setAnswerMap(String, String) | 0 | 1 | 1 | 1 |
Main.main(String[]) | 38 | 3 | 14 | 15 |
Question.Question() | 0 | 1 | 1 | 1 |
Question.Question(String, String, String) | 0 | 1 | 1 | 1 |
Question.check(String) | 0 | 1 | 1 | 1 |
Question.getAnswer() | 0 | 1 | 1 | 1 |
Question.getContent() | 0 | 1 | 1 | 1 |
Question.getNum() | 0 | 1 | 1 | 1 |
TestPaper.TestPaper(String) | 0 | 1 | 1 | 1 |
TestPaper.addQuestion(String, String) | 0 | 1 | 1 | 1 |
TestPaper.calculateScore() | 1 | 1 | 2 | 2 |
TestPaper.checkFullScore() | 2 | 1 | 3 | 3 |
TestPaper.checkTestPaper(List |
9 | 4 | 5 | 6 |
TestPaper.getScoreQuestions() | 0 | 1 | 1 | 1 |
TestPaper.getTestNum() | 0 | 1 | 1 | 1 |
TestPaper.setTestNum(String) | 0 | 1 | 1 | 1 |
scoreQuestion.getNum() | 0 | 1 | 1 | 1 |
scoreQuestion.getScore() | 0 | 1 | 1 | 1 |
scoreQuestion.scoreQuestion(String, String) | 0 | 1 | 1 | 1 |
scoreQuestion.setNum(String) | 0 | 1 | 1 | 1 |
scoreQuestion.setScore(String) | 0 | 1 | 1 | 1 |
Class | OCavg | OCmax | WMC | |
AnswerSheet | 3.6 | 13 | 18 | |
Main | 15 | 15 | 15 | |
Question | 1 | 1 | 6 | |
TestPaper | 2 | 6 | 16 | |
scoreQuestion | 1 | 1 | 5 | |
Package | v(G)avg | v(G)tot | ||
2.4 | 60 | |||
Module | v(G)avg | v(G)tot | ||
oop2 | 2.4 | 60 | ||
Project | v(G)avg | v(G)tot | ||
project | 2.4 | 60 |
解释:语文有答卷,测试卷,且每个卷子内都有多个属性信息,故在原来一的基础上增加了AnsweSheet和Testpaper类题目中要求和测试样例中存在多个测试卷和答卷故在main函数中开设新建arrylist类对每一类数据进行存储,便于后续的访问,question对比上一次也有了较大的改进不用数组用list存储便于内部元素的遍历和相应的修改,节省了内存空间,此外,读者可以注意到我添加了一个scoreQuestion类,顾名思义这是一个存储题目序号和其对应分数的类,至于为什么这么做是因为这样方便存储题目信息中的“序号-分数”对,在testPaoer类中仅需要要一个列表即可完成对每一道题目分数的存储,且testPaper中存在一个计算分数的方法可以实现对本身题目链表的遍历计算试卷的满分是否为100,main函数对比上一次并无太大的改动,仅仅添加对新文本读入的改动进行新的分割方式,main的复杂度或许有所上升。
3.三次设计
复杂度:
Complexity metrics | 周日 | 21 4月 2024 22:05:37 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
AnswerSheet.AnswerSheet(String, String) | 0 | 1 | 1 | 1 |
AnswerSheet.answerCheck(String, String) | 0 | 1 | 1 | 1 |
AnswerSheet.mapPrint() | 1 | 1 | 2 | 2 |
AnswerSheet.resultOfAnswerSheet(List |
66 | 14 | 21 | 26 |
AnswerSheet.setAnswerMap(String, String) | 0 | 1 | 1 | 1 |
Main.main(String[]) | 62 | 6 | 23 | 24 |
Question.Question() | 0 | 1 | 1 | 1 |
Question.Question(String, String, String) | 0 | 1 | 1 | 1 |
Question.check(String) | 0 | 1 | 1 | 1 |
Question.getAnswer() | 0 | 1 | 1 | 1 |
Question.getContent() | 0 | 1 | 1 | 1 |
Question.getNum() | 0 | 1 | 1 | 1 |
Student.Student(String, String) | 0 | 1 | 1 | 1 |
Student.getName() | 0 | 1 | 1 | 1 |
Student.getSid() | 0 | 1 | 1 | 1 |
TestPaper.TestPaper(String) | 0 | 1 | 1 | 1 |
TestPaper.addQuestion(String, String) | 0 | 1 | 1 | 1 |
TestPaper.calculateScore() | 1 | 1 | 2 | 2 |
TestPaper.checkFullScore() | 2 | 1 | 3 | 3 |
TestPaper.checkTestPaper(List |
9 | 4 | 5 | 6 |
TestPaper.getScoreQuestions() | 0 | 1 | 1 | 1 |
TestPaper.getTestNum() | 0 | 1 | 1 | 1 |
TestPaper.setTestNum(String) | 0 | 1 | 1 | 1 |
scoreQuestion.getNum() | 0 | 1 | 1 | 1 |
scoreQuestion.getScore() | 0 | 1 | 1 | 1 |
scoreQuestion.scoreQuestion(String, String) | 0 | 1 | 1 | 1 |
scoreQuestion.setNum(String) | 0 | 1 | 1 | 1 |
scoreQuestion.setScore(String) | 0 | 1 | 1 | 1 |
Class | OCavg | OCmax | WMC | |
AnswerSheet | 5.6 | 23 | 28 | |
Main | 24 | 24 | 24 | |
Question | 1 | 1 | 6 | |
Student | 1 | 1 | 3 | |
TestPaper | 2 | 6 | 16 | |
scoreQuestion | 1 | 1 | 5 | |
Package | v(G)avg | v(G)tot | ||
3.04 | 85 | |||
Module | v(G)avg | v(G)tot | ||
untitled11 | 3.04 | 85 | ||
Project | v(G)avg | v(G)tot | ||
project | 3.04 | 85 |
解释:相比于上一次题目,这一次题目又添加了不少内容,最重要的两个莫过于学生和信息删除的引入,学生基本信息的引入意味着我需要做一个方法能够通过其一项信息找到某位特定的学生,而删除信息使得判题流程变得更加复杂易错,需要判断题目的存在性质,引用存在性质,答案存在性质,且需要规划好三者的优先级,需要处理的文本量和异常错误输入变得更多起来。此时就不能只靠一部分的if—else结构来解决,这里就需要引入正则表达式对文本进行匹配的处理了,当检测到每一个line的某数据字母时会进行相应格式的匹配进行判误,优化了判误流程。
正则表达式(Regular Expression)是一种强大的文本处理工具,用于匹配字符串中的特定模式。在编程中,正则表达式常用于数据验证、搜索和替换文本等操作。
正则表达式的基本组成包括:
字符:
- 普通字符:包括所有可见字符,如数字、字母、标点符号等。
- 特殊字符:包括一些有特殊含义的字符,如
\
(转义符)、^
(匹配字符串的开始)、$
(匹配字符串的结束)等。限定符:
*
:匹配前面的字符零次或多次。+
:匹配前面的字符一次或多次。?
:匹配前面的字符零次或一次。{n}
:匹配前面的字符恰好n次。{n,}
:匹配前面的字符至少n次。{n,m}
:匹配前面的字符至少n次,但不超过m次。分组:
( )
:用于分组,匹配括号内的表达式。|
:用于选择,匹配括号内任一表达式。定位点:
^
:匹配字符串的开始。$
:匹配字符串的结束。\b
:匹配单词边界。\B
:匹配非单词边界。特殊字符类:
\d
:匹配数字(等价于[0-9]
)。\D
:匹配非数字(等价于[^0-9]
)。\w
:匹配单词字符(等价于[a-zA-Z0-9_]
)。\W
:匹配非单词字符(等价于[^a-zA-Z0-9_]
)。\s
:匹配空白字符(等价于[\t\n\x0B\f\r]
)。\S
:匹配非空白字符(等价于[^\t\n\x0B\f\r]
)。转义字符:
\
:转义字符,用于匹配特殊字符。
合理使用正则表达式使之前臃肿的文本处理的代码量变得更简洁高效,减少很多工作量,对此类文本处理的工作有奇效,除此之外,我在答卷(answerSheet)中引入一个Map类对每道题的答案进行存储,或许也可以采用新建一个answer类来进行list结构存储,我觉得那样未免有点麻烦,干脆使用了Hashmap存储key,value每一个key与value都是一一对应的,这不正好符合我的答案对应题目序号的要求么。
在Java中,
Map
是一个接口,它定义了键值对(key-value pairs)集合的抽象数据结构。Map
接口是 Java Collections Framework(JCF)的一部分,用于存储键值对,并允许根据键快速检索对应的值。Java 提供了几种不同的实现
Map
接口的类:
- HashMap:基于哈希表的实现,提供快速的查找、插入和删除操作。它不是线程安全的,不保证元素的顺序。
- TreeMap:基于红黑树(Red-Black Tree)的实现,它保持键的排序顺序,通常是通过键的自然顺序或者通过构造函数指定的比较器。它是线程不安全的,且要求键实现
Comparable
接口。- LinkedHashMap:它是 HashMap 的变体,其内部维护着一个运行于所有条目的双重链接列表,它按照插入顺序或访问顺序排序键值对。
map的使用大大简化了数据读入的流程增强了可读性,相比于上一代的scoreQuestion有了一定的提升
3.踩坑心得
for(int i=0;i<n;i++)
{
line=input.nextLine();
part=line.split("#[A-Z]:| ");
q1[i]=new question(part[1],part[5],part[3]);
}
这是第一次作业中踩的坑,读者仔细思考一下就会知道单纯的根据“#[A-Z]:”和“ ”极为容易造成误判,若是题目中出现空格程序将无法正确分辨出它是分割的空格或者是题目中自带的空格,只会机械的分割出信息并固定对下标1,3,5进行存储,会存储错误的信息出错。
修改办法是:使用相应正则表达式避免空格的误判
Pattern patterN=Pattern.compile("^#N:(\\d+) #Q:(.+) #A:(.*)$");
Matcher matcherN=patterN.matcher(line);
for(TestPaper t1:testPapers){
t1.checkFullScore();
//t1.checkTestPaper(allQuestion);
}
看到这肯定莫名其妙,看起来并无错误,但仔细读题目要求就会知道题目的输入并不是按顺序,但输出的时候却需要按顺序输出,若是直接按读入顺序进行下面的操作,测试结果与样例肯定会有所出入。
解决办法是:添加比较器
//排序题目
t1.getScoreQuestions().sort(Comparator.comparing(scoreQuestion::getNum));
这样题目就会按照要求进行输出。
if(line.charAt(1)=='T')
{
if(matcherT.matches()){
String[] parts = line.split(" ");
//分割出testNum
String[] nameAndSpecial=parts[0].split(":");
TestPaper t1=new TestPaper(nameAndSpecial[1]);
testPapers.add(t1);
//分割出分数和分数成绩
String[] questionNumberandScore=new String[(parts.length-1)*2];
for(int i=1,j=0;i< parts.length;i++){
String[] item=parts[i].split("-");
questionNumberandScore[j++]=item[0];
questionNumberandScore[j++]=item[1];
}
//添加题目
for(int k=0;k<questionNumberandScore.length;k+=2)
{
t1.addQuestion(questionNumberandScore[k],questionNumberandScore[k+1] );
}
}
else{
System.out.println("wrong format:"+line);
}
} else if (line.charAt(1)=='X') {
if(matcherX.matches()){
String[] sidAndName = line.split("[:\\-\\s]+");
for(int i=1;i<sidAndName.length;i+=2){
Student stu=new Student(sidAndName[i],sidAndName[i+1]);
allStudent.add(stu);
}
}
else{
System.out.println("wrong format:"+line);
}
} else if (line.charAt(1)=='S') {
if (matcherS.matches())
{
String[] answerInformation = line.split("[:\\-\\s]+");
AnswerSheet answerSheet=new AnswerSheet(answerInformation[1],answerInformation[2]);
answerSheetList.add(answerSheet);
for(int i=4;i<answerInformation.length;i+=3){
answerSheet.setAnswerMap(answerInformation[i],answerInformation[i+1]);
}
}
else{
System.out.println("wrong format:"+line);
}
} else if (line.charAt(1)=='D') {
if(matcherD.matches())
{
String[] parts=line.split("[:\\-\\s]+");
//添加匹配到的数字
for(int i=2;i<parts.length;i+=2){
deleteQuestions.add(parts[i]);
}
}
else{
System.out.println("wrong format:"+line);
}
} else if (line.charAt(1)=='N') {
if(matcherN.matches()){
String[] QuestionOf = line.split("[:\\s]+");
if(QuestionOf.length==5){
Question q=new Question(QuestionOf[1],QuestionOf[3],"no answer");
allQuestion.add(q);
}
else{
Question q=new Question(QuestionOf[1],QuestionOf[3],QuestionOf[5]);
allQuestion.add(q);
}
//System.out.printf("%s %s %s",QuestionOf[i],QuestionOf[i+2],QuestionOf[i+4]);
}
else{
System.out.println("wrong format:"+line);
}
}
}
在处理文本模块时,也踩了一个大坑,看我的代码每次判断第二个字符是某个字母再进行出来,然后再使用正则表达式进行匹配,但我一开始并未进行正则匹配,所以很多异常数据会被错误读入而不是输出预想中的“wrong format”。解决办法就是每次进入验证前添加一个格式验证。
4.改进建议
可以改进的地方很多,我的代码仍然不够完善
- 文本输入处理仍然使用split分割出,其实效率比较慢,后期应该采用matcher.find()捕获组进行获取,但由于学艺不精还不能在这里灵活运用故采取下策
- main函数中的代码太过冗长,如果需要优化,一些基本操作如文本处理可以建立一个判断类进行封装,增强代码的可读性
- 对于三种答案或者题目的判断过于笨拙累赘,最后一次的作业中有三种次序优先级的判断,但我使用了几个循环来进行遍历的判断,这无疑增加了时间复杂度,可以将这几个步骤放入本身list的判断
- 正则表达式的输入判断仍然存在一些错误,对于空字符和无字符当成异常数据处理,需要进一步的处理。
5.总结
通过这三次作业我学到了很多,正则表达式的运用,Map类的使用和处理,arraylist存储结构的运用,加深了对类与类之间关系的认知,类中属性行为的书写,文本输入输出的处理,某项数据根据属性进行比较,运用了比较器简化比较流程,同时我增强了对于封装的认知,它是OOP的一个重要概念,它将数据和操作数据的函数封装在一起,形成对象。这样可以提高代码的可读性和可维护性,同时也可以隐藏对象的内部实现细节,提高代码的安全性。通过封装,我们可以更好地管理对象的状态和行为,大大便利了我们对对象的操纵。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?