一、 前言
经过Java题目集1-3的练习,我对Java编程有了更加深入的理解和掌握。题目集涵盖了Java基础语法、面向对象编程、集合框架等多个方面,题量适中,难度逐步上升,让我在挑战中不断成长。
三次训练的知识点涉及Java的基本语法,包括变量、数据类型、运算符、控制结构、方法定义与调用,进一步深入到面向对象编程,包括类与对象的定义、封装、继承等,还包括集合框架的使用,包括ArrayList
、HashMap
等常用集合类的操作以及JAVA程序的输入输出等。
总的来说,这三次训练题量适中,难度由浅入深,难点主要体现在最后一题,即答题判题程序上,关于这点我将在下文详细描述。
二、 设计与分析
2.1 题目集1
前三题,设计一个风扇类Fan
类、创建一个学生类Student
类以及在Student
类计算成绩,考察对类与对象的使用以及对数组的基本运用,内容并不复杂,难度也较低,在此不作过多赘述。值得注意的是数组的创建与C语言不同:Student[] student = new Student[5];
同时,Java中使用new
关键字创建的对象不需要显式地使用delete来释放内存,因为Java有自动内存管理机制。当我们使用new
创建一个对象时,Java会在堆内存中为该对象分配空间。当这个对象不再被任何引用变量所引用时,它就被认为是垃圾,并且可以被垃圾回收器回收。垃圾回收器会定期检查堆内存中的对象,并回收那些不再被使用的对象的内存空间。
另外,目前,我在Java中对数据的读取借助的是Scanner
类。Scanner
类是Java中的一个实用类,用于从不同的输入源(如键盘输入、文件等)解析原始类型和字符串的简单文本扫描器。
Scanner input = new Scanner(System.in);
第四题考察的是关联类的使用,我认为重点主要在数据权限和变量引用访问上,由于Student
类中关联了Grade
类,而Grade
类中的变量都设定为私有(private
),在Student
类中无法直接使用,因此需要在Grade
类中提供公有(public
)函数,起提供数据接口的作用。
同时,我在设计这道题的时候,将输入存入Createstudent
类,输出存入Print
类,这样可以实现输入输出分开,同时将main函数简化,在总体上达到结构清晰简洁的目的。
2.2 题目集2
这个题目集的2、3两题,即汽车类Car类和求圆面积的Circle类较为简单,在此就不过多讲解。这里主要分析一下第一题,即手机按价格排序、查找。编写手机类(MobilePhone),要求该类实现Comparable接口,重写compareTo方法,实现按照price的大小来确定两个手机对象的大小关系。在这里需要用到Java的Comparable<T>
接口,这个接口被用来定义对象的自然排序。通过实现Comparable
//重写compareTo方法
public int compareTo(MobilePhone phone){ return Integer.compare(this.price,phone.price); }
2.3 题目集3
这个训练集第一题,考察面向对象编程(封装性),主要是设计一个Student
类,难度不高。
第二题考察对日期类的基本使用。在这一题中,关键点首先在于从规定的输入格式中提取出我需要的数据,这里我采用的是String类的split()方法。采用这个方法可以从输入的字符串中提取到我想要的数据,并存放到数组中。
//将输入按" "分割后存入arr[],提取出需要的两个日期 String[] arr = intput.nextLine().trim().split(" "); //将日期按"-"分割后存入part[],得到年、月、日 String[] part1 = line.split("-"); String[] part2 = str.split("-");
其次,对输入数据进行处理,提取到需要的信息后,我采用了LocalDate
类的parse(ChaeSequence text)
方法将输入的字符串解析为LocalDate
对象。LocalDate类是Java 8及以后版本中引入的,用于表示不带时间的日期(年、月、日)。
//将line和str转化为LocalDate对象 LocalDate startdate = LocalDate.parse(line); LocalDate enddate = LocalDate.parse(str);
第三,计算两个日期的天数差,在JAVA中正好提供了实现此功能的方法。在Java中,ChronoUnit.DAYS.between(Temporal startInclusive, Temporal endExclusive)
方法被用来计算两个时间点之间的天数差。这里,startInclusive
是起始时间点(包含在内),而endExclusive
是结束时间点(不包含在内)。需要注意的是,这两个参数都必须是实现了Temprol接口的对象,而LocalDate正是这样一个实现了该接口的类。
//输出日期差异 long daysBetween = ChronoUnit.DAYS.between(startdate, enddate);//相差天数
2.4 答题判题程序
在这三次的训练集中,答题判题程序贯穿始终,且具有一定的连续性,因此我单独开了一个模块进行分析与介绍。首先是根据题目画出类图:
在上述类图中,分别有Question
、ExaminationPaper
、AnswerSheet
、Input
、Output
以及测试类Main
。其中题目类(用于封装单个题目的信息)、试卷类(用于封装整套题目的信息)、 答卷类(用于封装答题信息)、输入类(用于接收输入的信息)、输出类(用于处理接收的信息并判题给出结果)。
由此,给出答题判题程序的顺序图:
通过上述顺序图,让我思路清晰的编写代码。从分析来看,主要难点就在于如何正确的接收信息、如何实现一种高效的判题思路并且打印结果、以及在存储信息时,采用哪种数据结构存储信息。
对于接收信息这个难点,根据题目样例:#N:1 #Q:1+1= #A:2的格式,采用String类的split()方法将其先按照#号分割并存入字符数组,再按照:号来获取所要保存的信息。示例代码如下:
//获取题目信息
String[] parts = input.split("#"); int questionNum = Integer.parseInt(parts[1].split(":")[1].trim()); String question = parts[2].split(":")[1].trim(); String standAnswer = parts[3].split(":")[1].trim(); questionList.put(questionNum, new Question(questionNum, question, standAnswer));
在次仅列举接收题目信息的样例代码。
在判断题目输入信息是否合法时,则采用了正则表达式,根据接收信息的格式,给出能正确匹配的正则表达式:
//题目信息:^#N:\s*\d+\s*#Q:\s*.*\s*#A:\s*.*$
//试卷信息:^#T:\d{1,2}(\s+\d{1,2}-\d{1,2})*$
//答卷信息:^#S:\d\s+\d{8}(\s+#A:[1-9][0-9]*-\s*.*)*$
//学生信息:^#X:(\d{8}\s+([\u4e00-\u9fa5]|[a-zA-Z]){1,5}-?)+$
在使用正则表达式时,要合理分割进行匹配。
在判题思路时,一开始因为题目要求简单,并且接收的信息只有题目的数量、题目内容、答题信息,所以此时判题思路是直接根据答题信息里面的答案以及题目号来取出题目进行判题即可。在后面两次题目集中,接收的信息则添加了试卷信息、答卷信息、学生信息以及删题信息。此时则是要以答卷信息为主,通过循环来取出答卷进行与试卷的匹配来判题。示例代码如下:
for (AnswerSheet sheet : answerSheet) {
String paperId = sheet.getpaperId();
String studentID = sheet.getStudentID();
ExaminationPaper paper = paperList.get(paperId); // 取出试卷
Map<Integer, String> answersList = sheet.getAnswerList(); //取出作答列表 #S
Map<Integer, Integer> questionScores = paper.getQuestionList(); //试卷题号与分值的哈希表 #T
int totalScore = 0;//总分
List<Integer> scores = new ArrayList<>();//存储判题之后的题目分值
//处理每道题的答题结果
for(Map.Entry<Integer, Integer> entry : questionScores.entrySet()){
int questionNum = entry.getKey(); // 试卷中题目的编号
int score = entry.getValue(); // 对应题目的分值
Question question = questionList.get(questionNum); //题目
String answer = answersList.getOrDefault(index,"");//答案
}
// 输出总分和每道题得分
if (!studentList.containsKey(studentID)) {
System.out.println(studentID + " not found");
}
else{
System.out.print(studentID +" "+studentList.get(studentID)+": ");
for (int i = 0; i < scores.size(); i++) {
System.out.print(scores.get(i));
if (i < scores.size() - 1) {
System.out.print(" ");
}
}
System.out.println("~" + totalScore);
}
}`
在上述代码中,主要是通过外循环中答卷中的试卷号来取出答卷,在内循环中通过试卷和答卷中的题目号进行判题以及赋分。
三、踩坑心得
3.1 数据结构的选择
在训练1中,我采用的是ArrayList
数据结构存储信息,而从训练2开始,我就基本上采取HashMap
来存储信息。原因就在于题目需求不同。在训练1中,要求根据题号顺序输出,而输入可能是无序的,因此我需要能快速实现排序的数据结构。而在训练2和训练3中,输出要求按照答卷信息顺序,因此不需要快速排序,而是需要方便存储的数据结构。
ArrayList
是一个动态数组,存储的是对象引用,并且这些对象引用在内存中是连续存储的。由于ArrayList
提供了get(int index)
方法,可以在O(1)时间内访问任意位置的元素,因此它适合进行各种排序操作。HashMap
是一个基于哈希表的数据结构,它存储的是键值对,并且键和值之间是通过哈希函数映射的。由于HashMap
不保证元素的顺序(即不保证按照插入顺序或键的自然顺序排列),因此对其进行排序需要一些额外的处理。
在训练2中,一开始我并不想改变数据结构,然而这使代码实现过程非常痛苦,代码异常复杂,而且操作十分不便,在查阅资料与互相讨论后,我决定改变数据结构,采取HashMap
存储,果然使代码实现过程简化了。同时,由于上文所说的排序原因,对HashMap
的排序需要额外处理,因此我也没有在一开始就采用HashMap
来存储题目信息,尽管这是很方便的。同样,在后面的训练中,对于便于用ArrayList
存储的数据,如判题结果,我依旧采用的是ArrayList
存储,而不是全局采用HashMap
。
3.2 代码可读性和复用性
在代码实现过程中,一开始我习惯于在主类中进行输入输出,而核心部分封装在类中。但是这样造成的后果是主函数异常复杂,而且输入输出都是可封装的代码块,如果全部在主函数中呈现,会使代码并不那么直观,因此,在后续的训练中,我均将输入输出封装到类中,分别为Inport和Output,不仅简化了主类,同时使代码总体上呈现结构化,美观化。同时,在代码连贯性上,后续的练习并不需要对main函数进行复杂的修改,而是可以定点修改具体的类和函数,类与对象的实例化也不需要在主类中进行。
四、改进建议
对于训练集1和训练集2中的最后一题,我一开始并没有将输入输出封装到类,在课上经过老师指导之后,我才开始学着封装代码。对此进行封装是非常有好处的,我会在之后的练习中贯彻封装思想。
对于训练集3的最后一题,新增了删除题目功能,我的处理是新建一个删除表,在判题时先搜索题目是否在删除表中,然后再进行相应的判题或输出。这样做的好处是,不用对原有的类进行改变,坏处是多了删除表的空间,同时需要维护删除表。经过思考,我认为可以不需要删除表,而是在题目类中新增一个布尔型数值flag,flag=true时,题目未删除,flag=flase时,题目已删除,这样我们就不需要一个删除表,直接根据题目的flag就可以进行题目是否删除的判定。这样做也有坏处,就是对于每一道题目,都多了一个flag指标,而在代码过程中,确实会略微简便一些。还有可以采取直接去掉要删除的题目的选项,匹配不到就是已被删除,但是这需要更复杂的代码和操作,同时对存储的表,删除了表项后会造成空白,可能会产生错误,因此不如之前我提出的两种方法简便,因此我便没有考虑这种方法。
五、总结
经过本阶段的三次练习,我对Java编程有了更深入的了解。从这三次题目集中,我学到了Java基础语法,包括变量、数据类型、运算符、控制结构、方法定义与调用;面向对象编程,包括类与对象的定义、封装、继承等;还有集合框架的使用,包括ArrayList、HashMap等常用集合类的操作;JAVA程序的输入输出;正则表达式;JAVA库函数的利用以及异常处理等多个方面,使我对JAVA语句的使用在短期内实现了快速进步和成长。
经过这段时间的学习,我认为在代码实现前对题目进行总体分析和设计是非常重要的,尤其在数据结构的选择方面,选择好的数据结构不仅能简化题目要求,还能节约时间,便于代码实现,这在我之前的学习中是没有注意到的一点。其次是对功能的具体处理,对功能的具体实现不一定要按照题目要求死板的实现,而是可以采取灵活处理方式,比如训练2中的删除题目,可以新建删除表,然后将题目进去匹配,不必非对题目表进行删除,造成题目表的空白。第三是我的代码能力,在之前的代码中,我的主类包含了输入输出,这使我的main函数异常复杂,我在这个过程中逐步改进代码,开始感受封装带来的好处和便利。同时,随着代码体量的逐步增长,我开始注重注释的书写,既可以便于遵循逻辑的连贯性,同时也增加了代码的可读性。