Java 程序设计作业集 4-6 阶段性学习总结
距离第一次接触 Java 面向对象编程已经过去快一个月了,从作业集 4 到作业集 6,三次作业做下来踩了无数的坑,也慢慢从 “写代码全靠套模板” 到能自己设计简单的类结构。说实话最开始老师布置博客作业的时候,我觉得很麻烦,不就是写代码吗,还要写博客总结。但是真的开始复盘的时候,才发现很多知识点我以为懂了,整理出来的时候才发现理解得还不透彻,也算是一次系统性的查漏补缺。按照课程要求,我把这三次作业的设计思路、遇到的问题、踩过的坑还有自己的反思整理成这篇博客,既是完成作业,也是给自己这一阶段的学习做个记录。
一、前言:三次作业整体概况
作业集 4 到作业集 6 是我们从面向过程思维转向面向对象思维的核心练习,三次作业层层递进,知识点环环相扣,难度也是逐步上升。从最基础的类定义、属性封装,到继承多态、抽象类与接口,再到集合框架的综合应用,每一次作业都是在上一次的基础上延伸,也让我慢慢感受到了面向对象编程的逻辑。
1.1 知识点与题量分布
作业集 4 是面向对象入门,一共 5 道 PTA 编程题,核心知识点包括类的定义、属性封装、构造方法、方法重载、对象的实例化与使用。题量不算大,但是对于刚从 C 语言转过来的我来说,思维转变是最大的坎。一开始写代码总想着先写 main 方法,然后一步步写逻辑,完全不知道怎么把功能拆到类里面,这次作业前后花了 3 个晚上才做完。
作业集 5 是继承与多态进阶,一共 6 道题,核心知识点有类的继承、super 关键字、方法重写、抽象类、接口、多态与向上向下转型。这次作业难度明显上升,尤其是抽象类和接口的区别、多态的使用场景,我一开始完全是懵的,不知道什么时候该用抽象类什么时候该用接口。有两道题我卡了很久,最后是对着课本例子一点点改才通过的,前后花了快四天。
作业集 6 是集合框架综合应用,只有 3 道大题,但是代码量比前两次加起来还多,核心知识点是 ArrayList、HashMap 集合的使用、自定义对象的存储、集合的遍历、简单的业务逻辑实现,相当于做一个小型的学生信息管理系统,需要自己设计实体类、管理类,实现增删改查功能。这次作业是最难的,不仅考语法,更考整体的设计能力,我中间改了好几次类结构,最后还有两个边界测试点没完全通过,算是留下了点遗憾。
1.2 难度与整体感受
三次作业总代码量大概一千行左右,比起之前面向过程的作业,代码量没多多少,但是思考的时间多了好几倍。最大的感受就是:面向对象不是语法,是思维方式,光背单词一样背语法没用,得真的去设计、去写、去踩坑才能慢慢理解。
从通过率来看,作业集 4 我是一次性全过的,作业集 5 提交了 5 次才全过,作业集 6 到最后还有两个边界测试点没通过 —— 输入空数据的时候程序会异常,我调了很久也没完全调好,后面会详细说这个问题。但即使没全做对,这次作业给我的收获也比前两次加起来都多,因为终于不是写零散的小功能,而是试着搭一个完整的小系统了。
二、作业设计与源码分析
按照课程要求,我用 SourceMonitor 做了代码度量,用 PowerDesigner 画了对应的类图。下面分别对三次作业的核心题目做分析,相关的工具截图我都插在了对应的位置。
2.1 作业集 4:面向对象基础 —— 从 “写函数” 到 “写类”
作业集 4 的核心题目是学生成绩信息管理和日期类设计,这两道题占了这次作业一半的分数,也是最能体现封装思想的题目,我就以这两道题为主来分析。
2.1.1 核心题目设计思路
学生成绩管理题,题目要求定义一个 Student 类,包含学号、姓名、三门课程成绩这些属性,还要有计算总分、平均分、判断成绩等级的方法。一开始我写的时候,直接把所有属性都设成了 public,main 方法里直接赋值,后来看到题目要求封装,才全部改成 private,然后补上 get 和 set 方法。
那时候我其实不太理解 “为什么非要把属性藏起来”,觉得直接访问更省事。直到后来写成绩赋值的时候,我加了判断:如果成绩小于 0 或者大于 100,就赋值为 0 并提示错误。那时候才明白封装的意义:把属性藏起来,只能通过指定的方法访问,就能在方法里加校验,避免非法数据进入,保证数据的安全性。
日期类的题目要求定义 MyDate 类,有年、月、日属性,还要有判断闰年、计算当月天数、日期合法性校验的方法。这道题我一开始是把所有判断逻辑都写在 main 方法里的,后来才移到类里面,做成成员方法。那时候还觉得 “把方法放到类里多此一举”,后来做多了才慢慢明白,把数据和操作数据的方法放在一起,就是类的核心意义 —— 高内聚。
2.1.2 SourceMonitor 代码度量分析
我用 SourceMonitor 对作业集 4 的所有源码做了度量,生成的报表截图如下(图 1)。
图 1 SourceMonitor 作业集 4 代码度量报表截图
从报表数据来看,作业集 4 我一共写了 5 个类,总有效代码行 327 行,注释行 42 行,注释率 12.8%。平均每个类的方法数是 3.2 个,平均圈复杂度是 1.76,最高圈复杂度是 3,出现在 Student 类的 getGrade 方法里。
一开始我看不懂圈复杂度是什么意思,后来对着教程查了才知道,圈复杂度越高,说明代码的分支越多,逻辑越复杂,越容易出 bug。我回头看了 getGrade 方法,里面写了四个 if-else if 分支,用来判断 90 分以上优、80-89 良、70-79 中、60-69 及格、60 以下不及格,一共五个分支,所以圈复杂度到了 3。当时写的时候觉得很简单,就是几个判断,没想到在度量工具里就是复杂度最高的方法。这也给我提了个醒:看起来简单的分支逻辑,积累多了也会让代码变复杂,后续维护会很麻烦。
另外还有一个指标是最大嵌套深度,我的代码里最大嵌套是 2 层,都出现在日期类的闰年判断里,先判断能不能被 4 整除,再判断能不能被 100 整除,再判断能不能被 400 整除,嵌套了两层 if。现在看其实可以把条件合并成一个布尔表达式,减少嵌套,不过当时刚学,能写对就不错了,根本没考虑代码质量的问题。
2.1.3 PowerDesigner 类图设计与反思
这次作业的类图我是用 PowerDesigner 画的,一开始画的时候闹了不少笑话。最开始我把 Student 类的所有属性前面都标了加号(public),后来想起封装的要求,才改成减号(private),然后把 get、set 方法标成 public。最终的 Student 类图如下(图 2)。
图 2 作业集 4 Student 类 PowerDesigner 类图
画日期类的时候,我一开始把判断闰年的方法画成了静态方法,后来想了想,闰年是针对具体某一个日期对象的,是对象的行为,不是类的行为,所以改成了普通成员方法。那时候对静态方法和实例方法的区别还模模糊糊的,画类图的时候又理了一遍,反而清楚了不少。
现在回头看这次的类图,其实设计得很粗糙,就是把属性和方法简单列出来,没有考虑类之间的关系 —— 那时候还没学继承,都是独立的类。但这是我第一次用工具画类图,算是迈出了设计的第一步。以前写代码都是直接上手写,从来没先画过图,这次先画图再写代码,反而思路清晰了很多,写的时候不会漏方法,也不会写着写着就乱了。
2.2 作业集 5:继承与多态 —— 面向对象的核心
作业集 5 是我学得最吃力的一次,核心题目是图形面积计算和员工工资计算,正好对应抽象类和接口的使用,还有多态的应用。
2.2.1 核心题目设计思路
图形面积计算这道题,要求定义一个抽象的 Shape 类,里面有抽象方法 getArea () 和 getPerimeter (),然后定义 Circle 圆形类和 Rectangle 矩形类继承 Shape 类,实现对应的方法,最后写一个工具类,能比较两个图形的面积大小。
一开始我拿到题的时候,第一反应是写两个独立的类,分别算面积周长,然后写两个比较方法。后来看到要求用抽象类和继承,才硬着头皮去设计父类。写的时候最困惑的是:为什么非要弄个抽象父类?两个类分开写不是更简单吗?直到写比较方法的时候才明白,有了父类,比较方法的参数就可以写成 Shape 类型,不管传进来的是圆形还是矩形,都能调用 getArea () 方法,不用写两个重载的比较方法。那是我第一次真切感受到多态的好处:同一个方法,能处理不同类型的对象,代码复用性一下子就上去了。
员工工资题要求定义 Employee 员工接口,有计算工资的抽象方法,然后定义 FullTime 正式员工类和 PartTime 实习生类实现这个接口,正式员工按月薪 + 绩效算,实习生按天算工资。这道题我一开始又搞混了,写成了抽象类,后来翻课本才记住:接口是定义行为规范的,这里 “计算工资” 是所有员工都有的行为,但是实现方式完全不同,用接口更合适。
经过这两道题,我自己总结了一个粗糙的区别方法:如果是 “是什么” 的从属关系,就用继承抽象类;如果是 “能做什么” 的行为规范,就用接口。不知道准不准确,但是目前做题是够用了。
2.2.2 SourceMonitor 代码度量分析
同样用 SourceMonitor 测了作业集 5 的代码,一共 7 个类(包括 1 个抽象类、1 个接口、4 个实现类、1 个测试类),总有效代码行 412 行,注释行 57 行,注释率 13.8%,比上次稍微高了一点,因为抽象方法和接口方法都加了注释说明功能。
图 3 SourceMonitor 作业集 5 代码度量报表截图
平均圈复杂度是 2.1,比作业集 4 高,最高圈复杂度是 4,出现在员工工资计算的测试类里。因为我写了一个简单的菜单,用 switch-case 选择输入不同类型的员工,分支多了,复杂度就上去了。
还有一个指标是继承深度,最深的继承链是 2 层,就是 Shape->Circle,Shape->Rectangle,都是直接继承,没有多层继承。老师上课说过 Java 不建议多层继承,会让类的结构变得复杂,难以维护,所以我也没敢写多层。
另外我发现,用了多态之后,代码的重复率低了很多。比如图形比较的方法,不用分别写圆形比较和矩形比较,只写一个就行。SourceMonitor 里的代码重复率是 2.3%,比我之前写面向过程的代码低很多,这应该就是面向对象的核心优势之一吧。
2.2.3 PowerDesigner 类图设计与反思
这次的类图比上次复杂了,有了继承和实现关系。图形类的类图如下(图 4)。
图 4 作业集 5 图形类继承关系类图
一开始画的时候,我把继承和实现的线搞混了,Circle 继承 Shape,我画成了虚线箭头,后来对着 PowerDesigner 的教程改了半天才弄对:实线空心三角是泛化关系(继承类),虚线空心三角是实现关系(实现接口)。
还有一个低级错误是,我一开始把抽象方法 getArea () 画成了普通方法,没有斜体显示,后来才知道抽象类和抽象方法在类图里是要斜体的,用来和普通方法区分开。
画员工类的类图的时候,我一开始把 Employee 画成了抽象类,后来改成接口,类图里的图标也不一样了,接口是用圆圈标出来的。这次画类图花了我快一个小时,一边翻笔记一边画,但是画完之后,整个类的结构一下子就清晰了,哪个类继承谁,哪个类实现什么接口,一目了然。写代码的时候就照着类图写,逻辑比之前清晰了很多。
2.3 作业集 6:集合综合应用 —— 从小功能到小系统
作业集 6 只有 3 道题,但是每道题都是综合题,我重点说最后一道学生信息管理系统,这也是这次作业占分最高的题,要求用集合实现学生信息的增删改查、按成绩排序、统计平均分这些功能。
2.3.1 核心题目设计思路
这道题我一共设计了三个类:
Student 实体类:封装学生的学号、姓名、三门成绩等属性,重写了 equals 和 hashCode 方法;
StudentManager 管理类:里面用 HashMap 集合存储学生,学号作为 key,学生对象作为 value,实现添加、删除、修改、查询、排序、统计这些业务方法;
Main 类:做菜单打印和用户交互,调用管理类的方法。
一开始我是想用 ArrayList 存的,后来想到学号是唯一的,用 HashMap 根据学号查找更快,就改成了 HashMap。现在想想,其实对于几十条的少量数据来说,两者速度没区别,但是正好练习一下 HashMap 的使用。
写管理类的时候,我一开始把所有功能都写在一个方法里,后来发现太乱了,找 bug 根本找不到,就拆成了 addStudent、deleteStudent、updateStudent、findStudent 这些单独的方法,每个方法只做一件事。这也是我从之前的作业里学到的教训:方法拆分清楚,出问题的时候定位快,改起来也方便。
2.3.2 SourceMonitor 代码度量分析
作业集 6 的代码量明显上去了,三个类加起来一共 587 行有效代码,注释行 76 行,注释率 12.9%,和之前差不多。
图 5 SourceMonitor 作业集 6 代码度量报表截图
平均圈复杂度是 2.8,最高圈复杂度达到了 6,出现在 StudentManager 类的 showMenu 方法里 —— 我把菜单显示和用户输入处理都放在这个方法里了,有 7 个功能选项,用 switch-case 实现,分支很多,所以圈复杂度很高。老师上课说过圈复杂度最好控制在 5 以内,超过 5 的方法就应该拆分,我这个方法确实有点臃肿,后面改进的时候可以拆开。
还有一个问题是,StudentManager 类的方法太多了,一共有 9 个方法,大部分都是业务方法,类的职责有点重。现在我还不知道怎么拆比较合理,听说以后会学分层架构,把数据操作和业务逻辑分开,应该就能解决这个问题。
另外,SourceMonitor 还提示我有两个方法的参数个数超过了 3 个,就是最开始写的添加学生方法,需要传学号、姓名、三门成绩,一共 4 个参数。老师说参数太多的方法不好维护,以后可以用对象传参,直接传一个 Student 对象进去,就不用传这么多参数了。我后来改了一下,确实更简洁,但是一开始写的时候根本没想到。
2.3.3 PowerDesigner 类图设计与反思
这次的类图涉及到了类之间的关联关系,StudentManager 类里有一个 HashMap 集合存储 Student 对象,所以两者是关联关系,也就是 “管理类包含学生对象”。管理系统的整体类图如下(图 6)。
图 6 作业集 6 学生管理系统类图
一开始我又画错了,把 StudentManager 和 Student 画成了继承关系,后来自己都觉得不对,学生和管理员怎么会是 “是一个” 的关系?翻了笔记才知道,“有一个” 的关系是关联,“是一个” 才是继承。关联关系用实线箭头表示,我还标了多重性,一个管理类对应多个学生,所以是 1 对多的关系。
画完类图我最大的感受是,以前写代码是想到哪写到哪,现在先画图,相当于先搭架子,架子搭好了再填代码,逻辑就不会乱。虽然我画的类图还很简单,很多关系都没画出来,但是比起最开始直接上手写代码,已经进步很多了。
三、采坑心得:那些让我调试到深夜的 bug
三次作业做下来,踩的坑数不胜数,很多 bug 现在想起来都觉得哭笑不得,但是每解决一个,就对知识点理解深一点。这些 bug 说出来都挺低级的,但是刚学的时候真的很容易犯,而且一卡就是很久,也算是学习必经的过程吧。我挑几个印象最深的坑总结一下,都有具体的测试数据和错误现象。
3.1 基础语法类问题:细节决定通过率
3.1.1 Scanner 输入的 “换行幽灵”
这个坑我在作业集 4 就踩了,到作业集 6 还差点又踩一次,印象特别深。
问题出现在学生信息录入的时候,代码逻辑是先输入 int 类型的学号,再输入 String 类型的姓名。我用 nextInt () 读学号,然后用 nextLine () 读姓名,结果每次运行到输入姓名的时候,程序都会直接跳过,根本不让我输入,姓名直接变成了空字符串。
当时我以为是循环写错了,把循环改了好几遍,又把变量名换了,都没用。折腾了快一个小时,最后去搜了一下才知道原因:nextInt () 只会读取输入的数字,不会读取数字后面的回车换行符;接下来的 nextLine () 会读取一整行内容,正好把剩下的空换行读进去了,所以姓名就成了空的。
我后来特意做了对比测试:
不加处理:输入学号 “2026001” 后回车,name 变量的值为 "",长度为 0,程序直接进入下一步;
在 nextInt () 后加一行scanner.nextLine()吃掉换行:姓名可以正常读取,输入什么就是什么。
就这么一行代码,我卡了一个小时。踩了这个坑之后我就长记性了,现在每次写混合输入的代码,都会特意注意 int 和 String 的顺序,再也不敢忘了 “吃换行”。
3.1.2 闰年判断的边界条件遗漏
还是作业集 4 的日期类题,判断闰年的逻辑,我一开始想当然写的是:能被 4 整除就是闰年。结果 PTA 提交之后,两个测试点直接挂了,分别是 1900 年和 2000 年的测试用例。
我当时百思不得其解,1900 能被 4 整除,怎么就不是闰年了?后来翻课本才看到完整的闰年判断规则:普通年份能被 4 整除且不能被 100 整除的是闰年;世纪年(整百年)能被 400 整除的才是闰年。1900 是世纪年,能被 100 整除但不能被 400 整除,所以不是闰年;2000 能被 400 整除,所以是闰年。
我把判断条件改成了:
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
再提交就全过了。这件事给我的教训特别深:写代码不能想当然,边界条件和特殊情况一定要考虑到,不然测试点肯定会挂。你觉得不会出现的情况,出题老师一定会考。
3.2 面向对象设计类问题:思维转变的阵痛
3.2.1 super () 必须在构造方法第一行
作业集 5 写 Circle 类的构造方法的时候,我需要调用父类 Shape 的构造方法给颜色赋值。我当时随手把super(color);写在了构造方法的第二行,第一行写了半径的赋值。结果编译直接报错,说 super 必须在构造方法的第一行。
我当时还挺不服气的,为什么非要在第一行?后来查了资料才明白,子类继承父类,子类对象里面其实包含了一个父类对象,必须先完成父类的初始化,才能初始化子类自己的属性,所以 super () 必须放在最前面。我把顺序换过来,先调用父类构造,再给半径赋值,编译就通过了。
这件事让我真正理解了 “继承” 不是简单的复制粘贴父类代码,子类的创建是依赖父类对象的。
3.2.2 向下转型的 ClassCastException
还是图形类的题,我写测试代码的时候,把一个 Rectangle 对象向上转型成了 Shape 类型,然后又想强转成 Circle 类型,调用 Circle 特有的 getRadius 方法。结果运行的时候直接报了ClassCastException,类型转换异常。
当时我以为只要都是 Shape 的子类,就能互相转,后来才明白,向上转型是自动的,但是向下转型是有风险的 —— 只有这个对象本身就是那个子类的实例,才能转成功。Rectangle 本来就不是 Circle,当然转不过去。
后来我学会了用instanceof先判断类型,再转型,就不会报错了。但是老师说,大量用 instanceof 说明类设计有问题,应该尽量用多态,少用强制转型。这句话我现在还在慢慢体会,还没完全吃透。
3.2.3 抽象类和接口的混用
员工工资那道题,我最开始把 Employee 写成了抽象类,里面放了 name、id 这些属性,还有抽象的计算工资方法。写完之后发现,正式员工和实习生的属性差很多,正式员工有 baseSalary 和 bonus,实习生有 daySalary 和 workDays,这些属性放在父类里不合适,子类用不上的属性也继承了,非常冗余。
后来我又改成了接口,只定义计算工资和显示信息的方法,属性都放在各自的实现类里。但是又发现,姓名和工号是两个类都有的,重复写了两遍,代码重复率又高了。
最后我改成了:一个抽象的 BaseEmployee 类,放公共的姓名、工号属性和构造方法;定义一个 SalaryCalculator 接口,里面有计算工资的抽象方法;两个子类再继承 BaseEmployee 并实现接口。虽然绕了一圈,但是经过这次折腾,我终于搞懂了抽象类和接口的定位:抽象类用来抽离公共的属性和方法,是模板;接口用来定义公共的行为规范,是能力。两者结合起来用,既能复用代码,又能规范行为。
3.3 集合应用类问题:综合场景暴露的短板
3.3.1 增强 for 循环删除元素:并发修改异常
作业集 6 里有个功能是删除成绩不及格的学生,我一开始用增强 for 循环遍历 HashMap 的 value 集合,然后判断平均成绩小于 60 就调用集合的 remove 方法删除。结果一运行就报ConcurrentModificationException,并发修改异常。
我当时懵了,什么是并发修改?我又没写多线程。后来查了才知道,增强 for 循环底层是迭代器实现的,遍历的时候,集合的结构不能发生改变(比如添加、删除),否则迭代器就会报错。
我做了测试验证:
增强 for 循环删除:10 个学生里删 3 个不及格的,运行必然报错;
迭代器的 remove () 方法删除:同样的测试数据,运行完全正常;
普通 for 循环倒序遍历删除:也能正常运行。
这个坑我印象特别深,以后遍历集合要删除元素的时候,再也不敢随便用增强 for 了。
3.3.2 ArrayList 去重失败:忘了重写 equals 和 hashCode
最开始做管理系统的时候,我用 ArrayList 存学生,要求学号不能重复。我用 contains () 方法判断学生是否存在,结果明明学号相同的学生,还是能重复添加进去。
我调试了半天,打印两个学生的学号,明明字符串是一样的,但是 contains 就是返回 false。后来才想起来,contains 方法底层是调用 equals 方法比较的,自定义类如果不重写 equals 方法,默认用的是 Object 类的 equals,比较的是对象的内存地址,不是内容。两个对象即使属性完全一样,地址不一样,也会被认为是不同的。
找到原因之后,我在 Student 类里重写了 equals 和 hashCode 方法,根据学号比较,再用 contains 就正常了。后来我改成用 HashMap,学号作为 key,就不用考虑这个问题了,但是这个知识点我是彻底记住了:自定义对象要比较内容,一定要重写 equals 和 hashCode。
3.3.3 异常处理缺失:输入不匹配直接崩溃
作业集 6 的菜单功能,要求用户输入数字选择功能。我一开始没做任何校验,如果用户不小心输入了字母,程序就会直接抛出 InputMismatchException 然后崩溃。
我测试的时候不小心输错了一次,程序直接终止了,之前输入的十几个学生信息全没了。那时候才意识到,程序的健壮性很重要,不能假设用户一定会输入正确的内容。后来我加了 try-catch 捕获输入不匹配的异常,提示用户输入错误,请重新输入,程序就不会崩了。
但是我现在只会简单的 try-catch,很多异常处理的知识还没学,比如自定义异常、抛出异常这些,后面还要慢慢补。
四、改进建议:让代码可持续优化
三次作业做下来,我自己也发现了很多问题,不管是设计上还是代码质量上,都有很大的改进空间。结合 SourceMonitor 的报表和自己写代码的感受,我总结了几点具体的改进方向。
4.1 结构优化:拆分复杂方法,降低圈复杂度
根据 SourceMonitor 的结果,我有好几个方法的圈复杂度偏高,比如作业集 6 的菜单方法,圈复杂度到了 6,超过了老师说的 5 的警戒线。
改进的方法就是拆分方法:把菜单显示和用户输入处理分开,写一个printMenu()方法专门打印菜单选项,再写一个handleChoice()方法处理用户的选择。这样每个方法的职责更单一,圈复杂度也能降到 3 以内。
还有成绩等级判断的方法,原来用多个 if-else if,分支多可读性差,可以改成用数组存储等级对应的分数区间,循环判断,代码会更简洁,也方便后续修改等级标准。
4.2 设计优化:强化面向对象设计原则
现在我的代码里,很多类的职责还不够清晰,比如 StudentManager 类,既管数据存储,又管业务逻辑,还管输出提示,有点 “全能类” 的感觉,以后改功能会很麻烦。
以后再写类似的系统,可以试着分层:比如写一个 StudentDao 类,专门负责数据的增删改查,也就是和集合打交道;再写一个 StudentService 类,负责业务逻辑,比如成绩统计、排序这些;然后界面层单独放,负责和用户交互。这样每层只做一件事,修改其中一层不会影响其他层,代码的可维护性会好很多。
另外,还要多利用多态和接口,减少 instanceof 判断。比如以后如果要加新的图形类型,不用修改比较面积的方法,直接继承 Shape 类就行,符合老师说的开闭原则。这些设计原则我现在还只是懂个大概,以后写代码要刻意去练习使用。
4.3 健壮性优化:完善异常处理与边界校验
现在我的代码,大部分都只考虑了正常输入的情况,异常情况考虑得很少。比如输入成绩的时候,输入负数、输入超过 100 的数、输入字母,这些情况我都没做完善的校验,要么数据非法,要么程序直接崩溃。
以后写代码,要养成做参数校验的习惯:比如方法接收成绩参数,先判断是不是在 0-100 之间,不是的话抛出异常或者返回错误提示;用户输入的时候,用 try-catch 捕获输入不匹配的异常,引导用户重新输入,而不是让程序直接崩掉。
还有边界值的测试,比如日期类的 2 月 29 号、12 月 31 号,集合为空的时候查询、删除,这些边界情况都要提前考虑到。作业集 6 我没通过的两个测试点,就是因为没考虑集合为空的情况,以后写代码要特意测一下边界场景。
4.4 规范优化:统一编码风格与命名规范
这一点是我自己的老问题,写代码的时候命名很随意,有时候变量名用 a、b、temp,过两天自己回头看都不知道是什么意思。还有代码的缩进,有时候乱了也不整理,看起来乱糟糟的。
以后要按照老师讲的 Java 命名规范来:类名大驼峰,方法名和变量名小驼峰,见名知意;代码缩进统一用 4 个空格;大括号的位置统一。还有注释,重要的方法和复杂的逻辑要写注释,说明功能、参数、返回值,不仅方便别人看,自己以后回头改也方便。
我下载了阿里巴巴 Java 开发手册,打算以后写代码就照着这个规范来,从一开始就养成好的编码习惯。
五、阶段总结与后续规划
5.1 本阶段学习收获
从作业集 4 到作业集 6,这一个月的学习,我最大的收获不是学会了多少 Java 语法,而是慢慢建立起了面向对象的思维。最开始写代码,我是 “步骤导向” 的:先做什么,再做什么,把所有逻辑都堆在 main 方法里;现在写代码,我会先想 “这个问题里有哪些实体?每个实体有什么属性和行为?实体之间是什么关系?”,先设计类,再写方法,最后写主逻辑。这个转变说起来简单,真的做到花了很久。

除此之外,我还学会了用工具辅助编程:用 PowerDesigner 画类图,先设计再编码,思路更清晰;用 SourceMonitor 度量代码质量,知道自己的代码哪里复杂度高,哪里需要改进。这些工具以前我听都没听过,现在虽然用得还不熟练,但是已经能感受到它们的用处了。

还有就是调试能力的提升,最开始遇到 bug 只会瞎改代码,现在会用打印变量值的方式一步步定位问题,虽然还不会用断点调试,但是比最开始强多了。踩过的那些坑,比如 Scanner 换行、并发修改异常、equals 重写,现在都成了我的经验,下次再遇到类似的问题,很快就能解决。
5.2 自身存在的不足
当然,我也清楚自己还有很多不足,差得还远。
首先是面向对象的设计能力还很差,遇到复杂一点的需求,就不知道怎么拆分类,不知道怎么设计继承和接口关系,很多时候都是写完了再改,改到能跑就行,没有提前规划好。
然后是代码的健壮性很差,几乎没有系统的异常处理,边界情况考虑不周,很多题目都是刚好通过正常测试点,一遇到边界数据就错。
还有就是知识面太窄,很多 Java 的核心特性都还没学,比如 IO 流、多线程、异常体系,这些都只是听过名字,还不会用。现在写的程序都是运行在内存里的,程序一关数据就没了,还不能持久化保存。
另外,调试工具用得不好,只会用打印输出调试,不会用 IDE 的断点调试,找 bug 效率很低,遇到复杂的 bug 要找很久。
5.3 后续学习计划
针对这些不足,我也给自己定了几个具体的小目标:
第一,多做面向对象的类设计练习题,刻意练习自己的抽象能力,拿到需求先画类图,再写代码,不要上来就敲代码。
第二,提前学习异常处理和 IO 流的知识,给现在的学生管理系统加上文件保存功能,让数据能存到本地文件里,下次打开程序还能读取。
第三,学习 Eclipse 的断点调试功能,提高排错效率,不能一直靠打印输出找 bug,太浪费时间了。
第四,坚持写代码的时候遵守编码规范,变量名见名知意,重要逻辑加注释,从平时的作业开始养成好习惯。
第五,有空的时候看看 Java 基础类的源码,比如 ArrayList 和 HashMap 的源码,虽然现在肯定看不懂大部分,但是先混个眼熟,慢慢理解底层实现原理。
总的来说,这三次作业让我对 Java 面向对象编程有了真正的理解,虽然踩了很多坑,走了很多弯路,但是进步也是实实在在的。接下来的学习里,我会继续补短板,多写多练,争取把面向对象的思想真正学透,而不是只会背语法。
浙公网安备 33010602011771号