一、前言
在学习了Java最基础的语法以及完成五次oop课前训练集之后,我们终于步入正轨,从符合人类思维的、流程化的,但存在可复用性差等缺点的面向过程的编程方式转入面向对象编程。谈到面向对象,就不得不谈及它的三大特点:封装、继承、多态。而这三次的题目尚未涉及继承和多态,主要是实现封装这一特性,即将数据和操作数据的方法封装在一个对象中,隐藏对象内部状态和实现细节,仅对外提供公共访问方式。在第一次题目集中的题目就要求要将数据域全部设为私有,并对数据域提供访问器和修改器。第一次题目集的7-1和7-2主要要求我们创建并使用单个类和对象,类的创建涉及属性和方法,而方法中包含无参和有参的构造方法,这两题中都要求定义输出信息的方法,都是创建对象并输出对象属性信息,题目较为简单。7-3则设计对象数组这一知识点,主要是处理同一类中的多个对象,还是较为简单的内容。而7-4中开始设计到多个类,主要考察类间的关联,难度稍有提升。本次题目集最据挑战性的题目即7-5答题判题程序-1,本题要求输入题目信息和答题信息,并通过题目信息的标准答案判断答题结果,本题主要考察类的设计以及类间关系,在进行输入时涉及字符串的处理、正则表达式的使用以及多余空格符的处理,但由于是第一次大作业,总体难度并不大,但有多种情况,较为费时。总体上,第一次题目集题量多但难度小,得满分较为容易。第二次题目集的7-2和7-3和第一次的前两题差不多,个人认为是拿来凑数的题,没有多大的价值,而7-1就有点意思了,这题涉及到的知识点包含接口,要求定义的类实现特定接口,至于难度,还是一道简单题。而7-4的答题判题程序-2是在前一次的基础上增加新的内容,输入的信息增多,比如新增了试卷信息,输出的内容也增多了,比如新增了得分情况的输出,情况更是增加了不少,如各类信息混合输入、多份答卷等,这些新增内容使本题难度明显增大。综合来看,题目集2的题量适中但题目难度较第一次大很多。第三次题目集中,7-1是水题就不多说了,7-2虽然是小题但是情况也不少,而且需要用到Java中的日期类,涉及字符串的处理器与判断,难度适中。而7-3答题判题程序-3是本题目集的重头戏,这次的题目依旧是在上一次基础上进行迭代,但难度有了极大的飞跃,这次的题目新增信息其实并不是很多,实现起来也并不是十分复杂,但相比前两次增加了输入内容的格式判断,需要灵活运用正则表达式,同时,无效信息以及一些特殊的输入情况也使本题更加复杂,在加上一些十分离谱的测试点,使本题难上加难,拿满分绝非易事,如果之前类的设计不合理,那么本此题目可能就要大改甚至重写。总的来说,第三次题目集题目量减少了,但难度提升了很多,涉及的知识点并未增多,主要还是类的设计及使用以及类间关系的综合运用。下面,我将对每一次题目集最后一题重点分析。
二、设计与分析
1、在第一次的答题判题程序中,我设计了三个类,分别是题目类、试卷类和答卷类,题目类用于封装单个题目信息,试卷类用于封装整套题目信息,而答卷类用于封装答题信息。题目类源码如下:
1 class Question{ 2 private int number;//题号 3 private String content;//题目内容 4 private String standardAnswer;//标准答案 5 public Question(){ 6 7 }//无参构造方法 8 public Question(int number,String content,String standardAnswer){ 9 this.number=number; 10 this.content=content; 11 this.standardAnswer=standardAnswer; 12 }//有参构造方法 13 public int getNumber(){ 14 return number; 15 }//题号的get方法 16 public void setNumber(int number){ 17 this.number=number; 18 }//题号的set方法 19 public String getContent(){ 20 return content; 21 }//题目内容的get方法 22 public void setContent(String content){ 23 this.content=content; 24 }//题目内容的set方法 25 public String getStandardAnswer(){ 26 return standardAnswer; 27 }//标准答案的get方法 28 public void setStandardAnswer(String standardAnswer){ 29 this.standardAnswer=standardAnswer; 30 }//标准答案的set方法 31 public boolean isRightAnswer(String ans){ 32 if(standardAnswer.equals(ans)){ 33 return true; 34 } 35 return false; 36 }//判题方法 37 }
该类中的属性包括题号,题目内容和标准答案,均为私有属性,并为各属性提供了get和set方法,实现了封装。该类提供了两个构造方法,但在本题中主要是用有参构造方法,即在创建题目对象时便确定题号、题目内容和标准答案,该类还提供了一个判断输入答案正确性的判题方法,即将输入的答案与该题目对象标准答案比较判断是否正确(事实上在本题中并未使用该判题方法)。该类的设计符合单一职责原则,属性均是与题目相关的必要属性,类名和方法名等均符合命名规范(如类名使用UpperCamelCase风格,方法名使用lowerCamelCase风格),该类的设计基本符合规范。
试卷类源码如下:
1 class Paper{ 2 private Question[] questionList=new Question[100];//题目列表 3 private int numOfQuestion;//题目数量 4 public Paper(){ 5 6 }//无参构造方法 7 public Paper(Question[] questionList,int numOfQuestion){ 8 this.questionList=questionList; 9 this.numOfQuestion=numOfQuestion; 10 }//有参构造方法 11 public void saveQuestion(Question question){ 12 int n=question.getNumber(); 13 questionList[n-1]=question; 14 }//保存题目 15 public void setNumOfQuestion(int numOfQuestion){ 16 this.numOfQuestion=numOfQuestion; 17 }//题目数量set方法 18 public int getNumOfQuestion(){ 19 return numOfQuestion; 20 }//题目数量get方法 21 public Question getQuestion(int k){ 22 return questionList[k]; 23 }//单个题目get方法 24 public Question[] getQuestionList(){ 25 return questionList; 26 }//题目列表get方法 27 public boolean isCorrectAnswer(AnswerSheet answerSheet,int i){ 28 String answer=answerSheet.getAnswer(i); 29 if(answer.equals(questionList[i].getStandardAnswer())){ 30 return true; 31 } 32 return false; 33 }//判题方法 34 }
该类包括题目列表和题目数量两个属性,题目列表为一个题目类对象数组,实现了将多个题目信息存储在试卷对象中,该类与题目类相关联,一张试卷有多个题目,一道题目在一张试卷中。在本题中使用的是无参构造方法,并通过saveQuestion这一方法将输入的题目逐个保存在试卷中,该方法以题目对象为参数,获取题目题号并保存在题目列表对应位置。该类还提供了一个判题方法,该方法将答卷类对象作为参数,获取题目列表中某一题对应答案与该题标准答案比较判断正确性(在本题并未用上),该类专门设计了一个获得单个题目的get方法,便于判题。
答卷类源码如下:
1 class AnswerSheet{ 2 private Paper paper;//试卷 3 private String[] answerList=new String[100];//答案列表 4 private boolean[] judgeList=new boolean[100];//判题列表 5 public AnswerSheet(){ 6 7 }//无参构造方法 8 public AnswerSheet(Paper paper,String[] answerList,boolean[] judgeList){ 9 this.paper=paper; 10 this.answerList=answerList; 11 this.judgeList=judgeList; 12 }//有参构造方法 13 public boolean[] judgeAnswer(Question question,int QuestionNum){ 14 int i=QuestionNum-1; 15 if(answerList[i].equals(question.getStandardAnswer())){ 16 judgeList[i]=true; 17 } 18 else{ 19 judgeList[i]=false; 20 } 21 return judgeList; 22 }//判题方法 23 public void saveAnswer(String ans,int QuestionNum){ 24 int k=QuestionNum-1; 25 answerList[k]=ans; 26 }//保存一个答案 27 public void setPaper(Paper paper){ 28 this.paper=paper; 29 }//试卷的set方法 30 public Paper getPaper(){ 31 return paper; 32 }//试卷的get方法 33 public boolean[] getJudgeList(){ 34 return judgeList; 35 }//判题列表的get方法 36 public String getAnswer(int k){ 37 return answerList[k]; 38 }//获取单个答案 39 public void outputResult(int QuestionNum){ 40 int m=QuestionNum-1; 41 Question ques=paper.getQuestion(m); 42 String q=ques.getContent(); 43 System.out.printf("%s~%s",q,answerList[m]); 44 }//输出方法 45 }
该类包括试卷对象、答案列表、判题列表三个属性,试卷对象为单张试卷,答案列表和判题列表为数组,该类与试卷类形成关联,一张试卷对应一张答卷。在本题中使用的是该类的无参构造方法,通过saveAnswer这一方法将输入的答案储存在答案列表。本题使用了该类的判题方法,即以题目对象和题号为参数,通过将答案列表中对应答案与该题目标准答案比较判断正确性,并将结果存入判题列表中。该类还有一个输出方法,即根据输入的题号在试卷对象中找到该题,获取该题内容,输出内容和答案列表中对应答案。
本题主类源码如下:
public class Main{ public static void main(String[] args){ Scanner input=new Scanner(System.in); int numOfQuestion=input.nextInt(); Question[] qu=new Question[100]; String[] str0=new String[100]; int i=0,num1=0; input.nextLine(); for(i=0;i<numOfQuestion;i++){ str0[i]=input.nextLine(); String[] str1=str0[i].split("#[NQA]:"); num1=Integer.parseInt(str1[1].trim()); String content1=str1[2].trim(); String standardAnswer1=str1[3].trim(); qu[i]=new Question(num1,content1,standardAnswer1); } Paper paper=new Paper(); paper.setNumOfQuestion(numOfQuestion); for(i=0;i<numOfQuestion;i++){ paper.saveQuestion(qu[i]); } AnswerSheet answerSheet=new AnswerSheet(); answerSheet.setPaper(paper); String s1=input.nextLine(); String[] s2=s1.split("#A:"); for(i=1;i<=numOfQuestion;i++){ answerSheet.saveAnswer(s2[i].trim(),i); } String s3=input.next(); for(i=0;i<numOfQuestion;i++){ answerSheet.outputResult(i+1); if(i!=numOfQuestion-1){ System.out.print("\n"); } } System.out.print("\n"); boolean[] isRight=new boolean[100]; int[] quNumber=new int[100]; for(i=0;i<numOfQuestion;i++){ isRight=answerSheet.judgeAnswer(qu[i],qu[i].getNumber()); quNumber[i]=qu[i].getNumber(); } Arrays.sort(quNumber,0,numOfQuestion); for(i=0;i<numOfQuestion;i++){ System.out.print(isRight[quNumber[i]-1]); if(i!=numOfQuestion-1){ System.out.print(" "); } } } }
在本题的主函数中,首先输入题目数量,再通过for循环输入每道题目的信息,并对输入的字符串解析,即通过split方法及正则表达式拆开字符串,同时在题目对象数组中创建新的题目对象,并将拆开后对应信息作为构造方法的参数,然后创建新的试卷对象,设置试卷题目数量和通过for循环保存每个题目,接着创建答卷对象,设置对应试卷,输入答案并通过split方法解析输入的字符串并将解析后的答案信息保存至答案列表中,然后outputResult方法输出题目内容和答案,最后通过for循环判题并用一数组记录题号和按题号排序(针对题目乱序输出情况下的判题),最后输出判题结果。主函数中涉及的数组均为定长数组,多次运用循环语句但无循环嵌套,使用了if语句控制格式,同时使用了Arrays类的sort方法,总体来看,主函数使用了创建的三个类中的方法,基本符合面向对象编程的思维。
2、在第二次的答题判题程序中,由于有新增的内容,我对第一次的代码进行了扩展和修改,但是并没有创建新的类,还是题目类、试卷类、答卷类三个类,其中题目类新增内容源码如下:
1 private int score;//题目分数 2 public int getScore(){ 3 return score; 4 }//分数的get方法 5 public void setScore(int score){ 6 this.score=score; 7 }//分数的set方法
题目类在原有基础上新增了分数这一属性,因为本次题目中输入的试卷信息包含每道题目的分数,同时增加了分数的set和get方法,其余内容不变,并未对该类进行修改,说明该类之前的属性和方法的设计是较为合理的,符合开闭原则(对扩展开放,对修改关闭)。
试卷类新增和修改的内容源码如下:
1 private int numOfPaper;//试卷号 2 private int[] scoreList=new int[100];//分数列表 3 public Paper(Question[] questionList,int numOfQuestion,int ppnum){ 4 this.questionList=questionList; 5 this.numOfQuestion=numOfQuestion; 6 numOfPaper=ppnum; 7 }//有参构造方法 8 public void saveQuestion(Question question,int index){ 9 questionList[index]=question; 10 scoreList[index]=question.getScore(); 11 }//保存题目 12 public void setScoreList(int[] scoreList){ 13 this.scoreList=scoreList; 14 }//分数列表的set方法 15 public int getCertainScore(int[] scoreList,int index){ 16 return scoreList[index]; 17 }//获取特定题目分数 18 public int[] getScoreList(){ 19 return scoreList; 20 }//分数列表的get方法 21 public void setNumOfPaper(int numOfPaper){ 22 this.numOfPaper=numOfPaper; 23 }//试卷号的set方法 24 public int getNumOfPaper(){ 25 return numOfPaper; 26 }//试卷号的get方法 27 public int getQuestionScore(int j){ 28 return scoreList[j]; 29 }/*获取分数列表特定题目分数,与上面的getCertainScore内容重复,之前写的时候没注意*/ 30 public boolean judgeFullScore(int sumScore){ 31 if(sumScore!=100){ 32 return true; 33 } 34 return false; 35 }//判断试卷总分是否为100分
在试卷类中,由于可能有多张试卷,便新增试卷号这一属性,同时设计题目分数和试卷总分判断,便新增题目的分数列表这一属性,同时试卷的有参构成方法的参数新增了试卷号这一内容(虽然本此题目并未使用该有参构造方法),考虑到输入内容的情况变得多样和复杂,对保存题目的方法进行了修改,参数新增了数组下标,可以将题目存储在题目列表的指定位置,同时将题目的分数存储至分数列表对应位置。新增了分数列表和试卷号的get和set方法,以及获取某一题分数的方法,但写了两个重复的方法,应删去一个。另外,新增了判断试卷总分的方法,因为题目要求试卷总分不是100分输出警示。总的来说,试卷类新增内容比题目类多,但没有大量的修改,说明设计还是比较合理的、可行的。
答卷类新增和修改内容源码如下:
1 private int numOfSheet;//答卷号 2 private boolean[] judgeOutcome=new boolean[100];/*判题结果列表,与judgeList重复,可将judgeList相关内容删去*/ 3 private int answerNumber;//作答题目数量 4 public void setNumOfSheet(int numOfSheet){ 5 this.numOfSheet=numOfSheet; 6 }//答卷号的set方法 7 public int getNumOfSheet(){ 8 return numOfSheet; 9 }//答卷号的get方法 10 public String[] getAnswerList(){ 11 return answerList; 12 }//答案列表的get方法 13 public void setJudgeOutcome(boolean[] judgeOutcome){ 14 this.judgeOutcome=judgeOutcome; 15 }//判题结果的set方法 16 public boolean[] getJudgeOutcome(){ 17 return judgeOutcome; 18 }//判题结果的get方法 19 public boolean getCertainOutcome(int t){ 20 return judgeOutcome[t]; 21 }//获得指定题目判题结果 22 public int getAnswerNumber(){ 23 return answerNumber; 24 }//作答题目数量的get方法 25 public void setAnswerNumber(int answerNumber){ 26 this.answerNumber=answerNumber; 27 }//作答题目数量的set方法 28 public void outputResult(int QuestionNum){ 29 int m=QuestionNum-1; 30 Question ques=paper.getQuestion(m); 31 String q=ques.getContent(); 32 judgeOutcome[m]=ques.isRightAnswer(answerList[m]); 33 System.out.println(q+"~"+answerList[m]+"~"+judgeOutcome[m]); 34 }//输出题目和判题结果方法 35 public int outputScore(int num){ 36 int totalScore=0,i=0; 37 for(i=0;i<num;i++){ 38 if(judgeOutcome[i]){ 39 Question ques=paper.getQuestion(i); 40 int qscore=paper.getQuestionScore(i); 41 totalScore+=qscore; 42 System.out.print(qscore); 43 } 44 else{ 45 System.out.print("0"); 46 } 47 if(i!=num-1){ 48 System.out.print(" "); 49 } 50 } 51 return totalScore; 52 }//输出得分的方法
在答卷类中,由于可能有多张答卷,故新增答卷号这一属性,由于考虑不周,新增的判题结果列表与之间的重复,由于会出现答案数量少于题目数量情况,故新增了作答题目数量这一属性,同时新增了相应内容的get和set方法,以及获得指定判题结果的方法,由于输出内容改变,故将输出题目和判题结果这一方法进行了修改,由于输出新增得分,故新增了输出得分方法,在该方法中使用了for循环和if语句判断,在输出方法中使用了题目类中的判题方法。总的来说,该类中由于题目变化新增了一些属性和方法,也修改了一些必须修改的方法,但总体上的设计还是较为合理的。
主类源码如下:
1 public class Main{ 2 public static void main(String[] args){ 3 int i=0,i2=0; 4 int num1=0,num2=0,n=0,count=0,count1=0,s=0,i1=0,sum1=0; 5 Scanner input=new Scanner(System.in); 6 int[] m=new int[50]; 7 int pnum=0; 8 Question[] qu=new Question[100]; 9 Paper[] paper=new Paper[50]; 10 AnswerSheet[] answerSheet=new AnswerSheet[50]; 11 //answerSheet.setPaper(paper); 12 String s1=input.nextLine(); 13 while(s1.equals("end")==false){ 14 if(s1.contains("#N")){ 15 String[] str1=s1.split("#[NQA]:"); 16 num1=Integer.parseInt(str1[1].trim()); 17 String content1=str1[2].trim(); 18 String standardAnswer1=str1[3].trim(); 19 qu[num1-1]=new Question(num1,content1,standardAnswer1); 20 count1++; 21 } 22 if(s1.contains("#T")){ 23 count=0; 24 paper[pnum]=new Paper(); 25 String[] str2=s1.split("(#T:|-| )"); 26 for(int j=2;j<str2.length;j+=2){ 27 int t=Integer.parseInt(str2[j].trim())-1; 28 int t1=Integer.parseInt(str2[j+1].trim()); 29 qu[t].setScore(t1); 30 paper[pnum].saveQuestion(qu[t],count); 31 paper[pnum].setNumOfPaper(Integer.parseInt(str2[1].trim())); 32 count++; 33 sum1+=t1; 34 } 35 int quNum=count; 36 m[i2]=quNum; 37 i2++; 38 paper[pnum].setNumOfQuestion(quNum); 39 if(paper[pnum].judgeFullScore(sum1)){ 40 System.out.printf("alert: full score of test paper%d is not 100 points\n",paper[pnum].getNumOfPaper()); 41 } 42 pnum++; 43 } 44 if(s1.contains("#S")){ 45 answerSheet[s]=new AnswerSheet(); 46 int cnt=0; 47 //answerSheet[s].setPaper(paper); 48 String[] s2=s1.split("(#A:|#S:)"); 49 for(int k=2;k<s2.length;k++){ 50 answerSheet[s].saveAnswer(s2[k].trim(),k-1); 51 cnt++; 52 } 53 answerSheet[s].setNumOfSheet(Integer.parseInt(s2[1].trim())); 54 answerSheet[s].setPaper(paper[Integer.parseInt(s2[1].trim())-1]); 55 answerSheet[s].setAnswerNumber(cnt); 56 s++; 57 } 58 s1=input.nextLine(); 59 } 60 int v=0; 61 int mark=0,nnum=0; 62 /*if(paper.judgeFullScore(sum1)){ 63 System.out.printf("alert: full score of test paper1 is not 100 points\n"); 64 }*/ 65 for(i1=0;i1<s;i1++){ 66 if(isMatch(answerSheet[i1],paper,pnum)){ 67 System.out.printf("The test paper number does not exist\n"); 68 continue; 69 } 70 for(i=0;i<m[v];i++){ 71 if(i>answerSheet[i1].getAnswerNumber()-1){ 72 System.out.printf("answer is null\n"); 73 mark=1; 74 nnum++; 75 continue; 76 } 77 answerSheet[i1].outputResult(i+1); 78 } 79 n=answerSheet[i1].outputScore(i); 80 if(mark==1){ 81 //for(i=0;i<nnum-1;i++){ 82 // System.out.printf(" 0"); 83 //} 84 } 85 System.out.print("~"+n); 86 if(i1!=s-1){ 87 System.out.print("\n"); 88 } 89 if(v<pnum-1){ 90 v++; 91 } 92 } 93 } 94 public static boolean isMatch(AnswerSheet answerSheet,Paper[] paper,int papernum){ 95 for(int i=0;i<papernum;i++){ 96 if(answerSheet.getNumOfSheet()==paper[i].getNumOfPaper()){ 97 return false; 98 } 99 } 100 return true; 101 } 102 }
相较于其他创建的类,本题中的主类的改动较大,内容也较为混乱,运用了较多的循环语句和if条件判断,还有一个明显的特点是定义了一堆变量,使得主函数结构相对复杂,还定义了一个判断试卷号是否错误的方法(事实上该方法不该在主类中定义),由于本次输入信息分为三种,故用了三个if语句判断输入内容是哪一种,然后再通过split方法解析输入的字符串,并创建新的对象存入对应对象数组中,同时进行相关处理。比如,答卷类保存解析出的答案至答案列表中,并设置答卷号、作答题目数量和试卷对象。而对于试卷信息则设置题目分数,保存题目,设置试卷号以及题目数量,并进行试卷总分的判断。而在输出内容时由于情况多,用了循环嵌套和if条件句控制,比如先判断试卷号是否存在,若存在则判断是否出现答案数少于题目数情况,若正常则输出结果和得分,否则执行continue语句。总的来说,本次主函数的设计还可以优化,可以考虑创建新的类来降低主函数复杂度。
3、在第三次答题判题程序中,由于有较多新增内容和格式判断,新增了答题学生类、验证类、输入(解析)类和删除题目类,并对原有类进行扩展和修改,其中题目类、试卷类基本保持不变,答卷类扩展和修改内容如下:
1 private String studentNumber;//学生学号 2 private AnswerStudent answerStudent;//答题学生 3 public void setStudentNumber(String studentNumber){ 4 this.studentNumber=studentNumber; 5 }//学生学号set方法 6 public String getStudentNumber(){ 7 return studentNumber; 8 }//学生学号get方法 9 public AnswerStudent getAnswerStudent(){ 10 return answerStudent; 11 }//答题学生get方法 12 public void setAnswerStudent(AnswerStudent answerStudent){ 13 this.answerStudent=answerStudent; 14 }//答题学生set方法 15 public void outputResult(int QuestionNum,int record){ 16 int m=QuestionNum-1; 17 Question ques=paper.getQuestion(m); 18 if(ques!=null){ 19 String q=ques.getContent(); 20 judgeOutcome[m]=ques.isRightAnswer(answerList[m]); 21 if(answerList[m]==null){ 22 System.out.print("answer is null\n"); 23 return; 24 } 25 System.out.println(q+"~"+answerList[m]+"~"+judgeOutcome[m]); 26 } 27 else{ 28 if(QuestionNum>this.getAnswerNumber()){ 29 System.out.print("answer is null\n"); 30 return; 31 } 32 if(record==0){ 33 System.out.printf("the question %d invalid~0\n",QuestionNum); 34 } 35 else{ 36 System.out.printf("non-existent question~0\n"); 37 } 38 } 39 }//输出答案内容和判题结果方法 40 public int outputScore(int num){ 41 int totalScore=0,i=0; 42 for(i=0;i<num;i++){ 43 if(judgeOutcome[i]){ 44 Question ques=paper.getQuestion(i); 45 if(ques!=null){ 46 int qscore=paper.getQuestionScore(i); 47 totalScore+=qscore; 48 System.out.print(qscore); 49 } 50 } 51 else{ 52 System.out.print("0"); 53 } 54 if(i!=num-1){ 55 System.out.print(" "); 56 } 57 } 58 return totalScore; 59 } 60 }//输出得分的方法
在答案类中,由于本题目要求新增学生信息,故新增了学生学号和答题学生对象两个属性,并提供get和set方法,由于输出的情况变得复杂,出现了引用错误与格式错误时的输出,故对输出方法进行了修改,如使用if条件句对题目对象引用是否为空进行判断,以及判断答案列表某一题答案是否为空进行判断,如果题目引用为空,则用if条件句对特殊情况进行判断,输出与之对应的信息。总的来说,答案类经过多次扩展,内容增加了不少,与答题学生类形成关联,方法的if语句增多,复杂度增大,但职责依然较为合理,没有与之无关的职责,但可以进行相关优化。
答题学生类源码如下:
1 class AnswerStudent{ 2 private String studentNum;//学生学号 3 private String studentName;//学生姓名 4 private AnswerSheet answerSheet;//答卷 5 public AnswerStudent(String studentNum,String studentName){ 6 this.studentNum=studentNum; 7 this.studentName=studentName; 8 }//有参构造方法 9 public String getStudentNum(){ 10 return studentNum; 11 }//学号get方法 12 public void setAnswerSheet(AnswerSheet answerSheet){ 13 this.answerSheet=answerSheet; 14 }//答卷set方法 15 public String getStudentName(){ 16 return studentName; 17 }//学生姓名get方法 18 }
该类有学生学号和学生姓名以及答卷对象三个属性,与答卷类形成关联,一张答卷对应一名答题学生。该类提供一个有参构造方法,以及属性的get/set方法,无其他方法,主要是由于该题中新增了学生信息的输入而新增了该类,设计较为简单。
删除题目类源码如下:
1 class DeleteQuestion{ 2 public static void delete(int qunum,Paper paper){ 3 Question[] question=paper.getQuestionList(); 4 question[qunum-1]=null; 5 } 6 }
该类是由于题目中存在输入删除题目信息的情况而新增的类,该类只提供了一个在试卷中删除题目的静态方法,事实上该类没必要创建,可以通过在题目类中增加新的属性和方法来达到删除题目信息的效果,该行为更适合题目类来实现。该类方法是针对试卷中的题目,在各类信息乱序输入时可能产生问题。
验证类源码如下:
1 class Validate{ 2 public static boolean isValidNumber(AnswerSheet answerSheet,AnswerStudent[] stu,int n){ 3 for(int i=0;i<n;i++){ 4 if(answerSheet.getStudentNumber().equals(stu[i].getStudentNum())){ 5 return true; 6 } 7 } 8 return false; 9 }//判断学号引用是否正确 10 public static boolean judgeFormat(String s){ 11 if(s.matches("#N:\\d+ #Q:.* #A:.*")){ 12 return true; 13 } 14 if(s.matches("#T:\\d+( \\d+-\\w+)+")){ 15 return true; 16 } 17 if(s.matches("#S:\\d+ \\d+( #A:\\d-.*)*")){ 18 return true; 19 } 20 if(s.matches("#D:N-\\d+")){ 21 return true; 22 } 23 if(s.matches("#X:\\d+ .*(-\\d+ .*)*")){ 24 return true; 25 } 26 return false; 27 }//判断输入格式是否正确 28 public static boolean isMatch(AnswerSheet answerSheet,Paper[] paper,int papernum){ 29 for(int i=0;i<papernum;i++){ 30 if(answerSheet.getNumOfSheet()==paper[i].getNumOfPaper()){ 31 return false; 32 } 33 } 34 return true; 35 } 36 }//判断试卷号引用是否正确
由于本题中新增了输入格式以及各种引用正确性的判断,于是新增了验证类来进行判断,提供了三个静态方法来判断,将上次在主类中定义的isMatch方法移至该类中,使类的设计更为合理,该类职责更为明确。
输入(解析)类源码如下:
1 class Input{ 2 public static Question[] parseQuestion(String s1,Question[] qu){ 3 int num1=0; 4 String[] str1=s1.split("#[NQA]:"); 5 num1=Integer.parseInt(str1[1].trim()); 6 String content1=str1[2].trim(); 7 String standardAnswer1=str1[3].trim(); 8 qu[num1-1]=new Question(num1,content1,standardAnswer1); 9 return qu; 10 }//解析题目 11 public static AnswerSheet[] parseAnswerSheet(String s1,Paper[] paper,AnswerSheet[] answerSheet,int s,HashMap<String,AnswerStudent>stuMap){ 12 int cnt=0,cnt1=0; 13 String[] s2=s1.split("(#A:|#S:| |-)"); 14 for(int k=5;k<s2.length;k+=3){ 15 cnt1=Integer.parseInt(s2[k-1].trim()); 16 if(s2[k].trim().isEmpty()){ 17 answerSheet[s].saveAnswer("",cnt1); 18 } 19 else{ 20 answerSheet[s].saveAnswer(s2[k].trim(),cnt1); 21 } 22 cnt++; 23 } 24 if(s1.charAt(s1.length()-1)=='-'){ 25 String str=Character.toString(s1.charAt(s1.length()-2)); 26 cnt1=Integer.parseInt(str); 27 answerSheet[s].saveAnswer("",cnt1); 28 cnt++; 29 } 30 answerSheet[s].setNumOfSheet(Integer.parseInt(s2[1].trim())); 31 answerSheet[s].setPaper(paper[Integer.parseInt(s2[1].trim())-1]); 32 answerSheet[s].setAnswerNumber(cnt); 33 answerSheet[s].setStudentNumber(s2[2].trim()); 34 answerSheet[s].setAnswerStudent(stuMap.get(s2[2].trim())); 35 return answerSheet; 36 }//解析答卷 37 public static Paper[] parsePaper(String s1,Question[] qu,Paper[] paper,int[] record,int[] m,int i2,int pnum,int[] sum1){ 38 int count=0; 39 String[] str2=s1.split("(#T:|-| )"); 40 for(int j=2;j<str2.length;j+=2){ 41 int t=Integer.parseInt(str2[j].trim())-1; 42 if(qu[t]==null){ 43 record[count]=t+1; 44 } 45 int t1=Integer.parseInt(str2[j+1].trim()); 46 if(qu[t]!=null){ 47 qu[t].setScore(t1); 48 paper[pnum].saveQuestion(qu[t],count); 49 } 50 paper[pnum].setNumOfPaper(Integer.parseInt(str2[1].trim())); 51 count++; 52 sum1[pnum]+=t1; 53 } 54 int quNum=count; 55 m[i2]=quNum; 56 paper[pnum].setNumOfQuestion(quNum); 57 return paper; 58 }//解析试卷 59 public static void paresDelete(String s1,int pnum,Paper[] paper){ 60 String[] s4=s1.split("(#D:|-)"); 61 int m1=Integer.parseInt(s4[2].trim()); 62 for(int k1=0;k1<pnum;k1++){ 63 DeleteQuestion.delete(m1,paper[k1]); 64 } 65 }//解析删除题目信息 66 }
由于主函数内容较为复杂,便试图将输入的字符串的解析写成方法,封装在一个新的类中,但由于之前主函数的规划设计不是很合理,便出现了用于解析的方法参数较多,方法体内部较为复杂的情况,如出现了多个if语句,而且使用的数组依然为定长的数组,而且解析答卷类中答案为空字符的处理还可以简化,后续可以进行相关的改进。
主类源码如下:
1 public class Main{ 2 public static void main(String[] args){ 3 int i=0,i2=0,pnum=0; 4 HashMap<String,AnswerStudent>stuMap=new HashMap<>(); 5 int[] record=new int[100]; 6 int[] sum1=new int[100]; 7 int num2=0,n=0,count1=0,s=0,i1=0,num3=0; 8 Scanner input=new Scanner(System.in); 9 int[] m=new int[100]; 10 Question[] qu=new Question[100000]; 11 Paper[] paper=new Paper[50]; 12 AnswerSheet[] answerSheet=new AnswerSheet[50]; 13 AnswerStudent[] answerStudent=new AnswerStudent[50]; 14 String s1=input.nextLine(); 15 while(s1.equals("end")==false){ 16 if(!Validate.judgeFormat(s1)){ 17 System.out.printf("wrong format:%s\n",s1); 18 } 19 else{ 20 if(s1.contains("#N")){ 21 Input.parseQuestion(s1,qu); 22 count1++; 23 } 24 if(s1.contains("#T")){ 25 paper[pnum]=new Paper(); 26 Input.parsePaper(s1,qu,paper,record,m,i2,pnum,sum1); 27 i2++; 28 pnum++; 29 } 30 if(s1.contains("#S")){ 31 answerSheet[s]=new AnswerSheet(); 32 Input.parseAnswerSheet(s1,paper,answerSheet,s,stuMap); 33 s++; 34 } 35 if(s1.contains("#X")){ 36 String[] s3=s1.split("(#X:| |-)"); 37 for(int k=1;k<s3.length;k+=2){ 38 answerStudent[num3]=new AnswerStudent(s3[k].trim(),s3[k+1].trim()); 39 stuMap.put(s3[k].trim(),answerStudent[num3]); 40 num3++; 41 } 42 } 43 if(s1.contains("#D")){ 44 Input.paresDelete(s1,pnum,paper); 45 } 46 } 47 s1=input.nextLine(); 48 } 49 for(int k=0;k<pnum;k++){ 50 if(paper[k].judgeFullScore(sum1[k])){ 51 System.out.printf("alert: full score of test paper%d is not 100 points\n",paper[k].getNumOfPaper()); 52 } 53 } 54 int v=0,mark=0; 55 for(i1=0;i1<s;i1++){ 56 if(Validate.isMatch(answerSheet[i1],paper,pnum)){ 57 System.out.printf("The test paper number does not exist\n"); 58 continue; 59 } 60 for(i=0;i<m[v];i++){ 61 answerSheet[i1].outputResult(i+1,record[i]); 62 } 63 if(Validate.isValidNumber(answerSheet[i1],answerStudent,num3)==false){ 64 System.out.printf("%s not found",answerSheet[i1].getStudentNumber()); 65 } 66 else{ 67 answerSheet[i1].outputStudentInfo(); 68 n=answerSheet[i1].outputScore(i); 69 System.out.print("~"+n); 70 } 71 if(i1!=s-1){ 72 System.out.print("\n"); 73 } 74 if(v<pnum-1){ 75 v++; 76 } 77 } 78 } 79 }
在本次的主函数中,依然存在变量较多的情况,使用的对象数组仍为定长数组,但对于答题学生信息的处理使用了HashMap。虽然将解析字符串的部分写成方法,但for循环和if条件句依然很多,尤其是在进行输出时由于情况较多,导致主函数结构更为复杂,降低了代码的可读性,可以考虑将内容的输出写成方法,封装在一个新的类中,使主函数更为清晰明了。
在这三次的类设计中,我逐渐掌握了类设计的方法以及单一职责原则的运用,也对类间关系有了更好的掌握,对正则表达式也更为熟练,更明白了什么样的类设计是好的设计,尤其是在多次迭代中,遵循开闭原则是较为重要的。
三、踩坑心得
在这三次的答题判题程序中,我遇到了不少问题,比如在第一次的答题判题程序中,出现了如下异常情况:
而出现该情况的主要原因是在使用nextInt()方法输入题目数量后直接使用nextLine()方法输入题目信息,事实上,nextInt()后紧跟着的nextLine()读取的是回车符,而不是题目内容,所以需要在nextInt()后再添加一个nextLine()用于读取换行符,在主函数中添加了"input.nextLine();"语句后,非零返回的问题便被解决。事实上,这是一个很容易出现的问题,在使用nextLine()输入字符串时一定要多加注意,尽量避免这种情况的发生。
除此之外,在第一次的题目中,还出现了如下异常情况:
该异常主要是由于在解析输入的题目信息字符串时,并未对使用split方法拆分后的字符串去除空格而直接使用Integer类中的parseInt()方法将字符串转为数字。所以需要使用trim()方法去除空白字符后再将字符串转为数字。所以,在处理字符串时一定要注意,尤其要关注空白字符的处理。
在第二次答题判题程序中,出现了如下异常情况:
该情况主要由如下一行代码导致:
1 paper[pnum++]=new Paper();
该异常主要是数组下标处理不当导致,在数组下标中错误地使用“[变量++]”这种形式,导致在创建新的试卷对象并赋给对象数组后变量pnum值加1,而后续的保存题目是所创建的试卷对象保存题目,即paper[pnum]保存题目,而此时pnum值加一,该下标对应数组元素的值为空,导致空指针异常,正确的做法应是在最后使pnum值加1。这提醒我处理数组时一定要注意下标的变化,使用ArrayList或许是一种更好的选择。
在第二次题目中,还出现了如下异常情况:
该情况主要由以下一行代码导致:
1 private int[] scoreList;
在试卷类中的分数列表是一个整型数组,但在定义该属性时忘记使用new关键字为数组分配空间和初始化数组元素,导致该数组的引用为空,而在后续使用了该数组,造成空指针异常的情况。这提醒我在创建数组时一定要使用new关键字进行初始化并赋值给该数组的引用,切不可只声明一个指向空的数组引用。
在第三次答题判题程序中,出现了如下异常情况:
该情况主要由以下一行代码导致:
1 for(int j=2;j<=str2.length;j+=2)
出现数组越界的异常主要原因是在进行试卷信息字符串解析时for循环结束条件判断错误,应将“<=”改为“<”。在处理数组时一定要注意数组元素下标是否越界,尤其实在for循环中以及使用数组对象的length属性时要特别注意。
四、改进建议
在第一次的答题判题程序中,在题目类、答卷类和试卷类中均有一个判题方法,但在主函数中只使用了答卷类的判题方法,可以对主函数进行修改,使其综合运用三种判题方法进行判题。在主函数中,题目对象数组可以使用集合类中的ArrayList数组,对于题目信息和答题信息字符串解析可单独写成方法,在主函数中调用该方法。另外,本程序定义了较多数组,可以稍作修改,如将quNumber这一数组删去,事实上,用该数组记录题目号再排序输出来应对乱序的题目信息输入是不太合理的,可以将排序的这部分内容删去,可以在创建新的题目对象时让对象数组下标与题号对应或采用其他方式进行改进,同时试卷类也可增加对应的输出方法。
在第二次的答题判题程序中,可以考虑增加一个答案类用于保存答卷中一道答案信息,提供计算得分的方法。在试卷类中增加获得总分的方法,在题目类中提供判断题目得分的方法,并将三个对象定长数组改为动态数组,同时删去不必要的变量,将解析字符串和最终的输出结果都可写成方法,可以考虑将这些方法封装在一个类中,使得整体的结构更为合理。
在第三次的答题判题程序中,可以考虑在题目类中新增题目是否有效的boolean型属性,同时新增设置题目失效的方法,并将删除题目类这一类删去,在本次题目中存在试卷中的题目序号与题目本身题号不一致的情况,可以考虑新增一个类保存试卷中的题目信息,在答案类中也可增加题目失效的方法。此外,字符串的解析和输出结果中存在循环多重嵌套以及多个条件判断,较为复杂,可以考虑重写或将其用多个方法来实现,以降低主函数的圈复杂度,同时尽可能将数组改为集合类中的列表,可以避免数组越界等问题。
五、总结
通过这三次的答题判题程序,我理解了基本的面向对象编程思想,认识到封装是面向对象编程的核心,自己创建的类中的所有属性都应该设置为私有属性,并学会了使用this关键字调用成员变量,理解了成员变量和局部变量的区别和引用变量的使用,更重要的是,在这三次题目中均设计了正则表达式,使我对正则表达式的使用更加熟练,但在这三题目中,对字符串的处理踩了不少坑,尤其是split方法的使用和空格的处理,在这方面需要多加练习,另外,对于java中集合类的使用还需进一步学习,可以优化程序。面向对象程序设计的基本原则还需进一步学习,目前只掌握了单一职责原则和开闭原则,但是在运用于类和类间关系的设计时还是会产生问题,比如对类中的属性设定可以进行优化,主函数的内容存在圈复杂度高的情况,还是会使用较多的循环和条件语句,这些都是需要改进的。至于课程的学习,我的建议是教师可以在课堂上选择一些比较典型的学生代码进行分析和提出改进的建议,同时对一些题目比较难通过的测试点进行讲解(比如第三次的最后一个测试点)。总之,在接下来的学习中,我会针对存在的问题加以改进,努力提升面向对象编程能力。
标签:题目,String,int,试卷,PTA,oop,方法,public From: https://www.cnblogs.com/23201218-lxl/p/18144386