面对对象程序设计前三次题目集总结
前言
关于面对对象
面向对象程序设计(Object-Oriented Programming,OOP)是一种编程范式,主要通过对象的概念来组织代码。它的核心思想是将现实世界的事物抽象为对象,通过对象之间的交互来实现程序的功能。而本学期我们学习的 JAVA 则是一种广泛使用的编程语言,支持完整的OOP特性。
关于本课程学习
在本学期中我们专业终于开始学习面对对象程序设计,而在此之前我从未使用过面对对象语言的方法来写程序,其实我也曾经萌生过提前自学面对对象语言的想法,但最后都被自己的懒惰给打败,直至今日,我才从课堂以及作业中真正开始学习面对对象语言。或许是由长时间写面对过程编程导致的思维固化,又或许是天赋不足,我在从面对过程编程到面对对象编程的转变中遇到了不小的挑战。故借作业为由,希望可以通过写下此篇 Blog 以提升自己对面对对象语言的理解费曼学习法。
关于题目
这三次作业总体来说不是很困难,其中每次作业的前几道题(除最后一题以外的题)都只需要一些比较简单的面对对象知识即可,知识点、难度、题量都还比较少,猜测可能是为了让我们熟悉面对对象思想而设立的。最后一题随着作业轮次的增加而逐渐增大难度,从三个类之间的简单调用,到多个类之间的复杂关系,难度系数逐渐增大,不过在目前阶段尚且可以应对。
设计与分析
作业1 7-1 设计一个风扇Fan类
设计
思路
根据题目要求,我们只需要创建一个 Fan 类,类中包含风扇当前状态等各项信息,最后在 main 函数中调用这个类即可。
类图
分析
报表数据
作业1 7-5 答题判题程序-1
设计
思路
首先分析题目,我们需要输入的信息有题目信息、答题信息,而我们需要输出的信息有结果,同时结果需要从题目信息的标准答案进行判断得出。因此我们可以设计 3 个类分别为题目类、试卷类、答卷类分别存储题目、试卷以及答卷的信息,对于结果我们可以设计方法将答卷的答案与题目中标准答案进行比较即可得出最后需要输出的结果。
类图
用户判断答案正确性顺序图
分析
报表数据
根据报表数据可发现,除 %Comments 外其它项基本符合要求。
题目类

1 // 题目类 2 class Topic { 3 private int topicID = 0; 4 private String content = "0+0"; 5 private String standardAnswer = "0"; 6 7 public Topic() { 8 }; 9 10 public Topic(int topicID, String content, String standardAnswer) { 11 this.topicID = topicID; 12 this.content = content; 13 this.standardAnswer = standardAnswer; 14 } 15 16 public Topic(String str) { 17 18 String[] parts = str.split("#N:"); 19 parts = parts[1].split(" #Q:"); 20 this.topicID = Integer.valueOf(parts[0].strip()); 21 str = parts[1]; 22 parts = str.split(" #A:"); 23 this.content = parts[0].strip(); 24 this.standardAnswer = parts[1].strip(); 25 26 } 27 28 public int getTopicID() { 29 return this.topicID; 30 } 31 32 public String getContent() { 33 return this.content; 34 } 35 36 public String getStandarAnswer() { 37 return standardAnswer; 38 } 39 40 public void setTopicID(int topicID) { 41 this.topicID = topicID; 42 } 43 44 public void setContent(String content) { 45 this.content = content; 46 } 47 48 public void setStandardAnswer(String standardAnswer) { 49 this.standardAnswer = standardAnswer; 50 } 51 52 public boolean Judge(String answer) { 53 if (answer.equals(this.standardAnswer)) 54 return true; 55 return false; 56 } 57 58 }
每个题目都有相应的编号、内容以及标准答案,故我们在题目类中设计三个属性分别存储编号、内容以及标准答案,同时我们应该设计一个构造函数来初始化一个题目。由于题目是通过输入格式化的字符串来代表题目的信息,所以我们应该通过设计一个方法来将字符串分解,将其中的信息提取出来并存储。又由于对于答卷中的每个题目答案我们都需要将其与相应题目的标准答案进行比较,为了方便,我可以直接在题目类中设计一个 Judge 方法进行判断,快速得到结果。
试卷类

1 // 试卷类 2 class ExaminationPaper { 3 private int tot = 0; 4 private ArrayList<Topic> topics = new ArrayList<Topic>(); 5 6 public ExaminationPaper() { 7 }; 8 9 public Topic getTopic(int num) { 10 return this.topics.get(num); 11 } 12 13 public void addTopic(Topic topic) { 14 tot++; 15 this.topics.add(topic); 16 } 17 18 public boolean Judge(int num, String answer) { 19 return topics.get(num).Judge(answer); 20 } 21 22 public void sortTopic() { 23 Collections.sort(topics, new Comparator<Topic>() { 24 public int compare(Topic a, Topic b) { 25 return Integer.compare(a.getTopicID(), b.getTopicID()); 26 } 27 }); 28 } 29 }
每一张试卷都有若干道题目,由于我们事先并不知道每张试卷都分别有哪些题目,故我们使用 ArrayList 动态数组对试卷中题目信息进行存储,同时我们使用了 tot 记录每张试卷中题目的数量,不过其实也可以不使用 tot 属性,因为我们也可以通过获取 ArrayList 的长度从而获得题目数量信息。为了方便,我们在试卷类中也设计了一个 Judge 方法,在这个方法中我们对于试卷中每个题目调用相应题目类中的 Judge 方法,使每个类都负责自己的职责。
答卷类

1 // 答卷类 2 class AnswerSheet { 3 private int tot = 0; 4 private ExaminationPaper examinationPaper = new ExaminationPaper(); 5 private ArrayList<String> answer = new ArrayList<String>(); 6 private ArrayList<Boolean> result = new ArrayList<Boolean>(); 7 8 public AnswerSheet() { 9 }; 10 11 public ExaminationPaper getExaminationPaper() { 12 return this.examinationPaper; 13 } 14 15 public void addAnswer(String str) { 16 tot++; 17 String answer = str.split(":")[1]; 18 this.answer.add(answer); 19 20 } 21 22 public boolean Judge(int num) { 23 if (examinationPaper.Judge(num, answer.get(num))) 24 return true; 25 return false; 26 } 27 28 public void addResult(int num) { 29 result.add(Judge(num)); 30 } 31 32 public String toString(int num) { 33 return examinationPaper.getTopic(num).getContent() + "~" + answer.get(num); 34 } 35 36 public void printInfo() { 37 for (int i = 0; i < tot; i++) { 38 System.out.println(toString(i)); 39 } 40 for (int i = 0; i < tot; i++) { 41 if (i > 0) 42 System.out.print(" "); 43 System.out.print(result.get(i)); 44 } 45 } 46 47 }
每个答卷都是在一张试卷上作答,所以我们在答卷中设计一个试卷类型的属性对相应试卷信息进行存储,同时对于试卷中的每个题目,我们需要知道其答卷中的作答,对于每个作答我们也需要记录其是否正确,所以我们又设计了两个属性将答案以及结果进行存储,并且我们使用了 ArrayList 便于动态更新这些信息。与另外两个类一样,我们在答卷类中也设计了一个 Judge 方法,通过其调用试卷类中方法进而调用题目类中方法,从而对每个答案进行判断。由于我们最后输出的信息与每张答卷有关,所以我们在答卷类中设计了 printInfo 方法,最后只需要调用这个方法即可对每张答卷最后的结果进行答应,极大地提高了程序的可读性以及可维护性。
作业2 7-4 答题判题程序-2
设计
思路
本次作业相对于上一次作业在试卷信息中增加了分数这一信息,因此我们需要在类中增加相应的属性对分数进行存储,同时设计合适的方法对分数进行判断,又由于程序输入的三种信息数据可能会被打乱,所以我们需要构思能够对被打乱信息进行正确存储的设计,如下。
类图
用户添加答卷信息顺序图
分析
数据报表
从图中数据可以得知,除 Methods/Class 偏高外其它各项基本正常。
题目类
此次作业中题目类与第一次作业题目类基本相同。
题目集类

1 // 题目集类 2 class Topics { 3 // 题目编号-内容 4 private HashMap<Integer, Topic> topics = new HashMap<>(); 5 6 public HashMap<Integer, Topic> getTopics() { 7 return topics; 8 } 9 10 // 添加题目 11 public void addtopic(String str) { 12 Topic topic = new Topic(str); 13 topics.put(topic.getTopicID(), topic); 14 } 15 }
由于这次作业中每张试卷都对应不同的编号的题目,并且题目的编号并不连续,而我们需要预先知道不同编号对应的是哪些题目,故我们选择在程序中增加了一个题目集类,在这个类中我们使用 HashMap 对程序中所有题目进行储存,每张试卷中对应编号的题目我们都可以在题目集类中查找,极大的方便了我们查找题目的需求。
试卷类

1 // 试卷类 2 class ExaminationPaper { 3 private int tot = 0; 4 private int examinationPaperID = 0; 5 private ArrayList<Topic> topics = new ArrayList<>();// 本张试卷的题目 6 private ArrayList<Integer> scores = new ArrayList<>();// 对应题目的分数 7 8 /* 9 *... 10 * 其它方法 11 * ... 12 */ 13 14 public boolean checkTheExamPapers() { 15 // 判断是否满100分 16 int allScore = 0; 17 for (Integer score : scores) { 18 allScore += score; 19 } 20 if (allScore != 100) 21 return false; 22 return true; 23 } 24 }
相较于第一次作业,这次作业中试卷中每个题目都增加了一个分数信息,并且相同的题目在不同的试卷中其分数是不相同的,故我们在试卷类中新增一个分数属性用于存储试卷中对应题目的分数信息,为了满足动态存储的需求,我们同样使用了ArrayList 作为属性的类型。由于对于每张试卷我们都需要判断其总分是否是 100 分,于是我们在试卷类中设计了一个checkTheExamPapers() 方法用于对试卷总分进行判断。
试卷集类

1 // 试卷集类 2 class ExaminationPapers { 3 private HashMap<Integer, ExaminationPaper> examinationPapers = new HashMap<>();// 编号对应的答卷 4 5 public HashMap<Integer, ExaminationPaper> getExaminationPapers() { 6 return examinationPapers; 7 } 8 9 public void addExaminationPaper(ExaminationPaper examinationPaper) { 10 examinationPapers.put(examinationPaper.getExaminationPaperID(), examinationPaper); 11 } 12 13 public void checkTheExamPapers() { 14 for (Map.Entry<Integer, ExaminationPaper> entry : examinationPapers.entrySet()) { 15 // System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); 16 if (!entry.getValue().checkTheExamPapers()) 17 System.out.println("alert: full score of test paper" + entry.getKey() + " is not 100 points"); 18 19 } 20 } 21 }
与题目集类相似,每张答卷都对应了不同的编号试卷,并且试卷的编号是不连续的,而我们需要预先知道不同编号对应的是哪些试卷,所有我们同样写了一个试卷集类,通过类中设置 HashMap 类属性,对应程序中的每张试卷进行存储,当我们需要使用某张试卷时,我们只需要通过寻找相应的 Key 值即可。
答卷类

1 //判断分数 2 int trueScore = 0; 3 for (int i = 0; i < examinationPaper.getTot(); i++) { 4 if (i > 0) 5 System.out.print(" "); 6 int score = 0; 7 if (result.size() > i && result.get(i)) 8 score = examinationPaper.getScore(i); 9 trueScore += score; 10 System.out.print(score); 11 }
答卷类与上次作业基本类似,我们只需要增加一个方法进行判断分数即可。
考试类

1 //考试类,包含所有题目以及试卷以及所有考卷 2 class Exam { 3 private Topics topics = new Topics();// 题目集,试卷中的题目是题目集的子集 4 private ExaminationPapers examinationPapers = new ExaminationPapers();// 试卷集 5 private ArrayList<AnswerSheet> answerSheets = new ArrayList<>(); 6 7 /* 8 *... 9 * 其它方法 10 * ... 11 */ 12 }
为了能够查找我们以及存储了哪些题目以及试卷,我们需要在考试类中分别创建题目集类以及试卷集类的属性,同时我们需要存储每张答卷的信息,于是我们需要再设置一个属性用于存储答卷,为了便于动态存储,我们需要使用 ArrayList 来进行存储,在我们输出信息时我们只需要调用每张答卷的方法即可。
作业3 7-3 答题判题程序-3
设计
思路
这次作业相较于第二次作业增加了学生姓名、学号,因此我们应该设计一个类在存储这些信息,同时程序还需要做到能够删除题目,并且需要知道某个题目是否被删除,因此我们还需要设计一个属性来存储被删除题目的标识。这次作业的输入格式相较以往变得更加复杂并且可能会出现错误输入,因此我们可以使用正则表达式来判断输入,增加程序的可读性。
类图
用户添加答卷信息顺序图
分析
报表数据
可以看到,我们程序中各项数据也基本正常。
题目类
题目类设计与第二次作业基本相同。
题目集类

1 // 题目集 2 class Topics { 3 /* 4 *其它属性 5 */ 6 private HashSet<Integer> deleteTopics = new HashSet<>(); 7 8 /* 9 ... 10 其它方法 11 ... 12 */ 13 14 // 删除题目 15 public void deleteTopic(String str) { 16 String[] parts = str.substring(3).split(" "); 17 for (String part : parts) { 18 int key = Integer.valueOf(part.split("-")[1]); 19 deleteTopics.add(key); 20 } 21 } 22 }
题目集类中我们新增了一个属性进行被删除题目的存储,由于被删除题目的编号并不连续,并且为了能够尽快查找某个题目是否被删除,我们将属性的类型设置成 HashMap 类型,方便对于被删除题目的存储以及快速查找,由于删除题目是题目集类应该有的职能,因此我们也在题目集类中增加了相应方法进行题目的删除。
试卷类
试卷类设计与第二次作业基本相同。
试卷集类
试卷集类设计与第二次作业基本相同。
学生类

1 // 学生 2 class Student { 3 private String studentID; 4 private String name; 5 6 /* 7 ... 8 一些方法 9 ... 10 */ 11 12 }
由于这次作业需要存储以及输出相关学生信息,所以我们新增了一个学生类,之后对学生的操作只需要调用学生类即可。
学生集类

1 // 学生集 2 class Students { 3 private HashMap<String, Student> students = new HashMap<>(); 4 5 /* 6 ... 7 其它方法 8 ... 9 */ 10 11 }
与题目集类相似,为了便于通过学号查找相应学生的个人信息,我们使用了 HashMap 对学生个人信息进行存储,当我们需要查找时我们只需要调用相应方法查找相应的键值即可。
答卷类
在第三次作业中,我们对每张答卷增加了相应的学生信息,因此我们在答卷中增加了每张答卷的学生信息作为答卷的一个属性,再在第二次作业的基础上增加些许对学生信息进行存储的操作即可。
考试类

1 // 考试类,包含所有题目以及试卷以及所有考卷 2 class Exam { 3 private Topics topics = new Topics();// 题目集,试卷中的题目是题目集的子集 4 private Students students = new Students();// 学生集,包含所有学生信息 5 private ExaminationPapers examinationPapers = new ExaminationPapers();// 试卷集 6 private ArrayList<AnswerSheet> answerSheets = new ArrayList<>(); 7 8 /* 9 ... 10 一些方法 11 ... 12 */ 13 14 }
由于对于每个学号我们都需要找到其对应的学生信息、对于每个题目编号要找到对于的题目信息、对于每张试卷我们需要找到对应的学生信息,故我们通过考试类将题目集类、试卷集类、学生集类进行调用,最后所有的操作我们只需要通过考试类进行相关方法的调用即可很方便的完成。
匹配类

1 // 通过正则表达式检测输入是否合法 2 class Match { 3 public static boolean checkRead(String str) { 4 ArrayList<String> checks = new ArrayList<>(); 5 checks.add("^#N:\\d+\\s+#Q:.+\\s+#A:.+$"); 6 checks.add("^#T:\\d+\\s+\\d+-\\d+(\\s+\\d+-\\d+)*$");// 最少有一题 7 checks.add("^#X:\\d{1,20} [\\u4e00-\\u9fa5a-zA-Z]{1,10}(-\\d{1,20} [\\u4e00-\\u9fa5a-zA-Z]{1,10})*$"); 8 checks.add("^#S:\\w+(\\s+\\w+)?(\\s+(#A:.+$))*?$");// 答案中可以出现空格 9 checks.add("^#D:N-\\d+$"); 10 11 for (String regex : checks) { 12 Pattern pattern = Pattern.compile(regex); 13 Matcher matcher = pattern.matcher(str); 14 15 if (matcher.matches()) 16 return true; 17 } 18 return false; 19 } 20 }
由于这次作业的输入可能会出现错误信息,所有我们可以用正则表达式很方便快捷地筛选出正确信息,因此我们在匹配类中只需要一个用于判断是否输入合法的方法即可。
踩坑心得
关于这些题
写完这三套题目我们可以发现,一、二、三次作业最后的一题都是关于考试判题程序的,并且后一次的作业都是在前一次作业上增加相应功能,这就要求我们做到对类职责的准确定义,并且从中我们可以发现面对对象编程的优越性,相较于面向过程编程,面对对象编程对代码的复用性更加好。因此第一个也是最重要的体会是,我们在写作业的时候要使用面对对象编程(因为这门课本身就是面对对象程序设计),同时我们在写代码之前应该进行分析,想好我们需要哪些类,每个类的职责是什么,这样我们才可以更好的应用面对对象编程,同时可以让之后的作业更加容易从这次作业上修改。
关于单个题
几个题目写下来,只有第三次作业的最后一题中坑点让我印象深刻。
坑点一:
答卷题目数目可以为0。
例如:
输入
\*
#N:1 #Q:1+1= #A:2
#T:1 1-5
#X:20201103 Tom
#S:1 20201103
end
*\
错误输出
正确输出
如果我们设置的正则表达式中不允许答卷出现题目数为0的情况,则会出错。
坑点二:
题目的答案可以包含空格。
例如:
输入
/*
#N:2 #Q:1+1= #A:2 #B:3
#T:1 1-5 2-2 3-9
#X:20201103 Tom-20201104 Jack
#S:1 20201103 #A:1-5 #A:2-2 #B:3
#D:N-2
end
*/
错误输出
正确输出
为了解决这种情况,我们应该重新设定正则表达式为 "^#S:\\w+(\\s+\\w+)?(\\s+(#A:.+$))*?$"即可解决问题。
如图:
从以上两个坑点我们可以发现,上述两个坑点都是由于正则表达式不正确导致的。我知道当输入很复杂的时候我们可以通过正则表达式判断输入是否符合要求,但是由于为了保证数据的健壮性我们应该仔细推敲正则表达式的正确性,否则可能会将正确输入误判为错误输入,导致与正确答案失之交臂。
改进建议
由于第三次大作业是由前两次大作业更改来的,因此在这里我们只讨论第三次大作业。
从第三次大作业中我们可以得知程序的输入输出是会出现乱序的,但是我们在第十个样例中可以看到下面这句话。
因此这题我们没有考虑引用的题目在被引用的信息之后出现的情况,但是实际上我们应该要在面对这种情况时依然做出正确的输出,故我建议我们在接收到答卷信息时先将接收到的信息单纯存储起来而不进行处理,在最后输出答卷信息时我们再统一处理,这样就变成了类似于引用的数据再被引用的信息之前给出的情况,最后我们就可以按照正确的格式进行输出。
总结
认识到了
前三次作业每次作业的题目数都在减少,但是我们可以感觉到难度在增加。对应每次作业的前几道题(除最后一题外的其它题),我们只需要简单地创建几个类即可快速完成,但是最后一题非常考察我们的代码能力,从第一次大作业开始,每次大作业的代码量都是以百行位单位进行增加的。例如,第一次大作业我的程序代码只有简单的166行,而第三次大作业我的程序代码却高达了563行,两者相差的代码量高达400行,从中我们容易想到随着大作业的进行,程序需要的代码里将会越来越多,而为了能够更加方便准确地进行作答,这就需要我们对程序划分出正确的类,并且对每个类的职能做到正确的划分,只有这样我们才能够最大程度地做到代码的复用,而面对对象编程的优点恰恰就在这里。
学习到了
这三次作业极大提高了我的编程能力,三次作业地迭代让我更加理解了面对对象程序设计的优势,同时庞大地代码量以及复杂地输入形式让我的思维更加灵活。
不足
这几次作业让我了解到正则表达式极其便利且丰富,但是两次的坑点让我发现我并没有完全掌握正则表达式,每次写一个正则表达式都要修修改改好几遍,但是这样让我认识到我在正则表达式这一方面还有进步空间,在接下来的时间中,我将会更加深入研究正则表达式的使用。