一、前言
1.知识点:
第一次题目初步考察了正则表达式,其中包括正则表达式的判断(matches函数)和分割(split函数)。初步考察了类与对象的设计,比如实体类(试卷类,题目类等)、控制类(改卷类等),考查学生对实际问题的需求提取与分析。
第二次题目进一步加强对上述各方面内容的考察。而且因为题目加入了S语句,使程序具备了基础的答题判分的机制,这使得答题判题程序开始需要用MVC模式去解决。
第三次题目加入了学生和删除题目的功能,使需求更复杂。第三次题目也是类与对象阶段考察的最后一次题目,从这次题目开始学生需要有意识、系统地设计各种类和类间关系,比如设计接口类、中介类,甚至可以提前设计抽象类等为之后的题目集做准备
2.题量:
第一二次的题量整体不算大,因为答题判题程序还尚不具备基础的功能。到第三次题目,答题判题功能初具雏形,题量加大很多。
3.难度:
第三次题目,程序已经初具雏形,而且对题目的操作更多,其所制造的需求也就更多,学生需要对各种情况判断准确,既不能漏输出也不能多输出。而且对格式的控制也必须严格,符号、换行、空格都不能有任何问题。以上导致了第三次题目在初学java阶段的难度较大,不仅要求学生要具备扎实的语法功底,还要求有基础的设计能力。
二、设计与分析
1.
前两次题目内容较简单,基本只需设计实体类和控制类(题目类,试卷类,改卷类等)就行,重点讲一下第三次题目的设计。
第三次题目,由于输入的信息较多,而且存在对信息的删改(如D语句,而且之后的题目集还可能出现偶然增加题目),所以必须设计数据处理类,将输入的各类信息先交由处理类保存,后续在设计改卷类、删题类等内容时,让这些类直接和处理类打交道,其实这里处理类也就相当于课上讲的雨刷实例中的中介类。
其他类只和处理类打交道而不直接和输入的各项语句打交道,可以有效避免信息的沉冗混杂,不仅能够提高程序的可辨识度,还可以使题目尽可能符合开闭原则,即对修改关闭对添加开放。这样后续再增加其他类(比如抽象类)可以不修改主类中对输入语句的处理(因为输入语句转化为修改类中的数据的方法是固定的)。
2.
while (!str.equals("end"))
{
str=scan.nextLine();
String N="#N:\\d+ #Q:(.+) #A:(.+)*";
String T="#T:\\d+( \\d+-\\d+)+";
String X="#X:\\d+ (\\w+\\w+)+(-\\w+ \\w+)*";
String S="#S:\\d+ \\w+( #A:\\w+-(.*)*)*";
String D="#D:N-\\d+";
if(str.charAt(1)=='N'||str.charAt(0)=='N'||str.charAt(2)=='N')
{
boolean flagN=str.matches(N);
if(flagN==false)
{
System.out.printf("wrong format:%s\n", str);
sentences[cnt]=str;
isExist[cnt]=1;
cnt++;
continue;
}
}
if(str.charAt(1)=='T'||str.charAt(0)=='T'||str.charAt(2)=='T')
{
boolean flagT=str.matches(T);
if(flagT==false)
{
System.out.printf("wrong format:%s\n", str);
sentences[cnt]=str;
isExist[cnt]=1;
cnt++;
continue;
}
}
if(str.charAt(1)=='X'||str.charAt(0)=='X'||str.charAt(2)=='X')
{
boolean flagX=str.matches(X);
if(flagX==false)
{
System.out.printf("wrong format:%s\n", str);
sentences[cnt]=str;
isExist[cnt]=1;
cnt++;
continue;
}
}
if(str.charAt(1)=='S'||str.charAt(0)=='S'||str.charAt(2)=='S')
{
boolean flagS=str.matches(S);
if(flagS==false)
{
System.out.printf("wrong format:%s\n", str);
sentences[cnt]=str;
isExist[cnt]=2;
cnt++;
continue;
}
}
if(str.charAt(1)=='D'||str.charAt(0)=='D'||str.charAt(2)=='D')
{
boolean flagD=str.matches(D);
if(flagD==false)
{
System.out.printf("wrong format:%s\n", str);
sentences[cnt]=str;
isExist[cnt]=1;
cnt++;
continue;
}
}
sentences[cnt]=str;
cnt++;
}
这一部分代码是用来判断错误格式输入并输出错误语句的,其实这一部分完全可以做成一个格式类,专门用正则判断格式是否正确,后续题目集若是有其他判断格式的需求出现可以只修改这个格式类,这样的代码才符合开闭原则。
for(int j=1;j<cnt;j++)
{
str=sentences[j];
if(isExist[j]==2)
{
for(int i=0;i<=4;i++)
{
if(str.charAt(i)>='1'&&str.charAt(i)<='9')
{
tmp=String.valueOf(str.charAt(i));
papernum=Integer.parseInt(tmp);
break;
}
}
for(int i=1;i<=TquestionCNT[papernum];i++)
{
System.out.println("answer is null");
}
boolean studentExist = true;
for(int i=0;i<1000;i++)
{
if(student1[i].getStudentNum()==studentNum)
{
System.out.printf("%d %s: ", studentNum,student1[i].getStudentName());
break;
}
if(i==999)
{
System.out.printf("%d not found", studentNum);
studentExist=false;
System.exit(0);
}
}
if(studentExist==false)
break;
for(int i=1;i<=TquestionCNT[papernum];i++)
{
if(i==1)
System.out.printf("0");
else System.out.printf(" 0");
}
System.out.printf("~0");
continue;
}
if(isExist[j]==1)
{
continue;
}
if(str.charAt(1)=='N')
{
inputQuestionNum++;
String[] NQA = str.split("#N:|#Q:|#A:");
NQA = removeBlank(NQA);
if(NQA[2]==null)
{
question1[Integer.valueOf(NQA[0].strip())] = new Question(Integer.valueOf(NQA[0].strip()), NQA[1].strip(), null);
}
question1[Integer.valueOf(NQA[0].strip())] = new Question(Integer.valueOf(NQA[0].strip()), NQA[1].strip(), NQA[2].strip());
}
if(str.charAt(1)=='X') //学生这里是从零开始排列的
{
str=str.substring(3);
string=str.split("-");
for(int i=0;i<string.length;i++)
{
string1=string[i].split("\\s+");
tmp=string1[0];
studentNum=Integer.parseInt(tmp);
student1[i].setStudentNum(studentNum);
tmp=string1[1];
student1[i].setStudentName(tmp);
}
}
if(str.charAt(1)=='S')
{
checkNum++;
string=str.split("\\s+");
int []questionnum1=new int [string.length-2];
tmp=string[0];
papernumstring=tmp.substring(3);
papernum=Integer.parseInt(papernumstring);
int flag1=1;
for(int i=1;i<=10;i++)
{
if(paper1[papernum][i].getScore()!=0)
{
flag1=0;
}
}
if(flag1==1)
{
System.out.printf("The test paper number does not exist\n");
continue;
}
tmp=string[1];
studentNum=Integer.parseInt(tmp);
for(int i=3;i<=string.length;i++)
{
tmp1=string[i-1].substring(3);
string1=tmp1.split("-");
tmp=string1[0];
questionnum1[i-3]=Integer.parseInt(tmp); //questionnum1数组从0开始
tmp=string1[1];
inputAnswer=tmp;
if(TquestionNum[papernum][questionnum1[i-3]]==0)
{
continue;
}
paper1[papernum][TquestionNum[papernum][questionnum1[i-3]]].setInputAnswer(inputAnswer);
}
for(int i=3;i<=TquestionCNT[papernum]+2;i++)
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf("answer is null\n");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null)
{
System.out.printf("non-existent question~0\n");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()!=null&&paper1[papernum][TquestionNum[papernum][i-2]].getScore() !=0&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()==0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf("the question %d invalid~0\n",TquestionNum[papernum][i-2]);
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]]))
{
System.out.printf("%s~%s~true\n", question1[TquestionNum[papernum][i-2]].getQuestion(),paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer());
actualSocreSum+=paper1[papernum][TquestionNum[papernum][i-2]].getScore();
}
else
{
System.out.printf("%s~%s~false\n", question1[TquestionNum[papernum][i-2]].getQuestion(),paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer());
}
}
boolean studentExist = true;
for(int i=0;i<1000;i++)
{
if(student1[i].getStudentNum()==studentNum)
{
System.out.printf("%d %s: ", studentNum,student1[i].getStudentName());
break;
}
if(i==999)
{
System.out.printf("%d not found", studentNum);
studentExist=false;
}
}
if(studentExist==false)
break;
for(int i=3;i<=TquestionCNT[papernum]+2;i++)
{
if(i==3)
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf("0");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null)
{
System.out.printf("0");
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]])&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf("%d", paper1[papernum][TquestionNum[papernum][i-2]].getScore());
}
else
{
System.out.printf("0");
}
}
else
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf(" 0");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null&&paper1[papernum][TquestionNum[papernum][i-2]]!=null)
{
System.out.printf(" 0");
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]])&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf(" %d", paper1[papernum][TquestionNum[papernum][i-2]].getScore());
}
else
{
System.out.printf(" 0");
}
}
}
System.out.printf("~%d\n", actualSocreSum);
actualSocreSum=0;
}
}
这一部分就是程序的核心了,对输入的各个语句进行处理。这一部分完全应该做成数据处理类,主函数只用来将每一条语句存到字符串数组中并将其传给处理类,不然主函数会显得冗杂、不好处理。
public static String[] removeBlank(String[] strs) {
int count = 0;
for (String each : strs) {
if (!each.equals("")) {
count++;
}
}
String[] str = new String[count];
for (int i = 0, index = 0; i < strs.length; i++) {
if (!strs[i].equals("")) {
str[index] = strs[i];
index++;
}
}
return str;
}
这个我自定义的方法是用来去除N语句中的空格的,这一部分最好也要移动到处理类,总之主函数只用来输入输出,所有和数据有关的内容全部传给数据处理类,再由该类和其他类打交道。
三、踩坑心得
1.
一定要仔细审题! 一定要仔细审题! 一定要仔细审题!(最重要的事说三遍真的都不够)。
因为审题有遗漏或者偏差,导致误解题目要求,进而使需求分析产生错误,这让我对程序的某些板块改了又改。而且一定不要自己去猜题目的意思,这大概率是不准的。有疑问就去看题目要求,而且要结合测试样例去看,因为题目的文字叙述比较抽象,测试用例不仅很直观,而且还能指导输出的格式要求。
for(int i=3;i<=TquestionCNT[papernum]+2;i++)
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf("answer is null\n");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null)
{
System.out.printf("non-existent question~0\n");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()!=null&&paper1[papernum][TquestionNum[papernum][i-2]].getScore() !=0&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()==0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf("the question %d invalid~0\n",TquestionNum[papernum][i-2]);
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]]))
{
System.out.printf("%s~%s~true\n", question1[TquestionNum[papernum][i-2]].getQuestion(),paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer());
actualSocreSum+=paper1[papernum][TquestionNum[papernum][i-2]].getScore();
}
else
{
System.out.printf("%s~%s~false\n", question1[TquestionNum[papernum][i-2]].getQuestion(),paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer());
}
}
boolean studentExist = true;
for(int i=0;i<1000;i++)
{
if(student1[i].getStudentNum()==studentNum)
{
System.out.printf("%d %s: ", studentNum,student1[i].getStudentName());
break;
}
if(i==999)
{
System.out.printf("%d not found", studentNum);
studentExist=false;
}
}
if(studentExist==false)
break;
for(int i=3;i<=TquestionCNT[papernum]+2;i++)
{
if(i==3)
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf("0");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null)
{
System.out.printf("0");
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]])&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf("%d", paper1[papernum][TquestionNum[papernum][i-2]].getScore());
}
else
{
System.out.printf("0");
}
}
else
{
if(paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()==null)
{
System.out.printf(" 0");
continue;
}
if(question1[TquestionNum[papernum][i-2]].getQuestion()==null&&paper1[papernum][TquestionNum[papernum][i-2]]!=null)
{
System.out.printf(" 0");
continue;
}
if(doTest1.judge(paper1[papernum][TquestionNum[papernum][i-2]], question1[TquestionNum[papernum][i-2]])&&paper1[0][TquestionNum[papernum][i-2]].getWhetherExist()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getScore()!=0&&paper1[papernum][TquestionNum[papernum][i-2]].getInputAnswer()!=null)
{
System.out.printf(" %d", paper1[papernum][TquestionNum[papernum][i-2]].getScore());
}
else
{
System.out.printf(" 0");
}
}
}
比如输出的题目顺序是S中对应T的次序!而不是T中题号顺序所以需要存一遍T中题号顺序。
- 即使认真地审完题之后,也不要着急去编程。一定要先设计类和各种类的成员、方法,这直接决定了你的程序是否符合七大原则。如果直接上手,程序很容易不符合单一职责原则、开闭原则,这两个原则只要没问题,后续编程基本一路畅通!!!(题目本身不考过难的算法,考的是类的设计,如果类都设计不好就上手编程,这肯定是本末倒置的。既不容易拿满分,也让后续对代码的修改的难度随着题目迭代指数级增加)。
string1=new String[100];
string1=string[i-1].split("-");
questionnum=Integer.parseInt(string1[0]);
numstring=string1[1];
score=Integer.parseInt(numstring);
paper1[papernum][questionnum].setScore(score);
titledScoreSum[papernum]+=score;
TquestionNum[papernum][i-1]=questionnum;
上述代码是我在对T语句分析中写的,虽然暂时解决了题目中要求的“答案的顺序号与试卷信息中的题目顺序相对应”,“答案数量可以不等于试卷信息中题目的数量,没有答案的题目计0分,多余的答案直接忽略,答案之间以英文空格分隔”这些需求,TquestionNum[papernum][i-1]=questionnum用于记录T中出现的题目各个题号的顺序,但是经过复盘第三次题目集后,我发现了这个方法看似实用实际上存在隐患。因为接下来迭代的题目集会更新很多陌生需求,比如TquestionNum[papernum][i-1]中的题号有删除,就这一点会让题目需要修改不少内容。
所以程序中凡是对于数组的使用,其实最好还是使用链表,链表对迭代的适应性更强。
if(str.charAt(1)=='N')
if(str.charAt(1)=='X')
if(str.charAt(1)=='S')
if(str.charAt(1)=='T')
if(sentences[j].charAt(1)=='D')
上述设计看似分项处理了输入的各种语句,其实也存在很大隐患。比如第三次题目中我的S分支需要用到T分支中的数组,但是假如T分支的数组发生变动,会使S分支中大量相关语句需要修改,甚至S分支运行不下去,更不用说S分支不和T分支在一个循环语句中,或者T语句在输入时在S语句之后,T分支的数组尚为空S分支同样运行不了。
- 第三次程序代码较多,因为一开始没有写明足够多注释,导致debug的时候太容易搞不清楚某一部分的意义。
- 越长的程序,debug的地位就越重要,尤其是程序有迭代需求。前两次题目集我对debug功能的使用很不熟练,有时甚至“明明debug勘误很简单却因为用不明白而硬着头皮一行一行脑算”。而且对debug一些细节功能没有理解和运用。
四、改进建议
1.
最最重要的:添加数据处理类等,让主函数所有和输入输出无关的内容统统放到相应的类中(第三次题目我只设计了实体类和控制类,这是远远不够的),并且各个类也要符合单一职责原则,有时间和精力的话最好再添加抽象类等,为接下来考察继承与抽象类的题目集做准备。
2.
归根结底,还是要将while循环中各个分支语句写入数据处理类,只有提前把输入的语句处理汇集一遍,再传给其他类,才能避免数组为空对某种输入的处理语句无法运行。
3.
之后的题目集中,哪怕每个类已经符合单一职责原则,在那个类中不同的模块旁(或者临时添加的变量)加明注释。
五、总结
1.
不难看出,这三次题目集考察的重点不是算法,而是对实际问题的建模和对大量需求的分析,面对这种题目,设计的时间不能被编程的时间压缩,一定要保证设计合理之后再着手编程,不然桥再多代码也是南辕北辙(哪怕这一次题目集能满分,迭代之后的题目集很可能及格分都拿不到)。
单一职责原则(Single Responsibility Principle, SRP)
- 定义:一个类应该只有一个引起它变化的原因。
- 目的:提高类的内聚性,降低类的复杂度。
开闭原则(Open/Closed Principle, OCP) - 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 目的:允许软件扩展新功能,而不需要修改现有代码。
迪米特法则(Law of Demeter, LoD) - 定义:一个对象应该对其他对象有最少的了解。
- 目的:减少对象之间的耦合,提高模块的独立性。
以上是第三次题目集我认为最重要的三个基本原则。初学者往往认为这些原则用处不大,而且本来就对这些原则很陌生就更不愿意再多花时间设计,这是大错特错。这些原则在软件设计过程中非常重要,它们帮助我们创建更加灵活、可维护和可扩展的软件系统。在实际开发中,遵循这些原则可以减少代码的复杂性,提高代码的质量。
- 完成前三次实验另一个很深的体会就是加深了对工程思维的认知。
工程思维(Engineering Thinking)是一种解决问题和决策的方法论,它强调逻辑性、系统性和实用性。
其实java的七大原则都是对工程思维的拓展与应用,我们在处理复杂实际问题时不能只着眼于该问题本身,还要考虑它可能随环境和时空变化而发生的迭代、演化,时刻保持工程思维才能完美应对实际问题每一次的迭代。工程思维不仅仅适用于传统的工程领域,如建筑、机械、电子等,也适用于软件工程、商业管理、公共政策等各个领域。在当今快速变化的世界中,工程思维的重要性愈发凸显,它帮助我们更好地应对复杂问题和挑战。