PTA1-3作业总结复盘

一、前言

这三次作业是一个比一个麻烦。第一次还算基础,第二次开始加约束条件,第三次直接上物理公式,重心那块我算了三四遍才跟样例对上。

第一次作业是个货物配载程序。给航班号、最大载重、货物清单,按重量降序排完序再装,最后输出装了哪些货、总重量、超载状态。主要知识点是类的定义、对象实例化、List容器、Comparator排序。题量不大,一共5个类,大概一百多行。难度属于入门级,难度较低。

第二次作业明显复杂了。这次有多个货舱,每个货舱有自己的格子布局,货物要指定装到哪个货舱。程序先按重量排序,再依次尝试装入目标货舱,需要判断每个货舱是否超载,以及航班总重是否超过最大起飞重量和最大业载。用到了HashMap存货物和货舱的映射关系,排序我手写了一个冒泡排序(其实可以用sort但想练手)。难度中等偏上,主要是输入数据多,条件分支容易乱,需要理清逻辑思维。

第三次作业难度最大,做了一个航班载重平衡系统。说实话我一开始连力臂和力矩都记不清了,专门回去翻了一下物理书。题目给了前货舱和后货舱,两个力臂不一样,旅客有固定体重和力臂,空机也有自己的重量和力臂。需要算总力矩除以总重量得到重心位置,再换算成MAC百分比,最后判断在不在安全范围内(25%到38%)。涉及的知识点有封装(第三次全部是private)、ArrayList(没加泛型,编译老报警告)、手写排序、输入合法性校验。物理公式那块我卡得最久,后来手算了几组数据才理解公式的逻辑。

三次作业的题量:第一次5个类,第二次7个类,第三次也是7个类左右。代码行数从一百多行涨到三百多行,体量递增。

二、设计与分析

第一次作业分析

image

第一次作业我分了5个类。Main类是入口,负责输入解析、对象创建和流程调度。Cargo类是货物实体,存了货物名称和重量,当时偷懒字段直接用了public。Flight类是航班实体,存航班号和最大载重,同样也是public字段。CargoSorter是排序类,封装了按重量降序排序的逻辑,里面直接调用了Comparator的comparingDouble方法。LoadManifest是配载单类,维护一个货物列表和一个总重量字段。

说实话第一次作业写得比较糙。Cargo和Flight的字段直接用了public,没做封装,当时觉得方便就行,没考虑数据被外部随意修改的风险。LoadManifest里同时维护了货物列表和总重量,其实总重量完全可以通过遍历货物列表实时计算,单独存一个字段反而容易导致数据不一致。

c55716f44657ac431fdce4c26700ef90
3bfb6fffc4dc9d5fb63f3f00849eff7d

从报表看,第一次作业圈复杂度整体偏低,Main类大约在4左右,其他类基本都是1或2。代码嵌套深度不大,最深的地方也就是Main里的for循环套了一个if判断。CargoSorter虽然复杂度低,但它直接调用了现成的Comparator,排序逻辑其实没自己写。

第二次作业分析

image

第二次作业的类数量增加了。Position类是位置类,负责把行列索引转换成类似A1、B2这种舱位编号。Cargo类和第一次差不多,但字段改成了private,算是一个进步。CargoCompartment是货舱类,包含了id、最大载重、格子列表、已装货物列表这几个属性。Flight类是航班类,管理多个货舱。LoadDispatcher是调度类,负责货物的排序和分配。Main类还是主入口。

第二次作业一个明显的改进是字段都改成了private,加了getter方法,封装性比第一次好了。但仍有不足,CargoCompartment里每次调用getCurrentWeight方法都要遍历已装货物列表求和,时间复杂度是O(n),如果货物数量大效率就不理想。
9aaaaf054a8a90788cca39a42fc25236
3683241325ee2332ebd5d1cd64927a2d

排序部分LoadDispatcher里手写了一个冒泡排序,两层for循环嵌套,相邻元素比较,重量小的往后换,最终实现降序排列。这个冒泡排序时间复杂度O(n²),而且实际开发中没必要手写,直接用Collections.sort配合Comparator更简洁。不过手写一遍有助于理解排序原理,也算有收获。

另一个问题是Main类承担了太多职责:输入解析、对象创建、装载逻辑、输出格式化全挤在一起,代码显得臃肿。按照单一职责原则,应该把这些逻辑拆分到不同方法或类中。

第二次作业圈复杂度比第一次高了,Main类大约在7到8之间,因为里面有多个条件判断分支。findCompartment方法通过遍历货舱列表来查找,时间复杂度O(n),如果货舱数量多可以考虑用Map来做索引。

第三次作业分析

第三次作业的类图:

image

第三次作业的类设计相对完整。Traveler是旅客类,包含姓名、行李箱重量、总重(75公斤基础体重加上行李重量)。Suitcase是行李箱类,只封装了一个重量字段。Product是货物类,有编号和重量。Storage是货舱类,包含编号、行列数、最大载重、当前载重、货物列表。AirTrip是航班类,管理前后两个货舱和旅客列表。Sorter是排序类,对货物按编号升序排列,里面又是手写的冒泡排序。BalanceCalc是平衡计算类,封装了重心计算、MAC百分比换算、安全评估这几个方法。Main还是主类。

这次引入了力臂和力矩的物理概念。前货舱力臂12.0米,后货舱力臂22.0米,旅客力臂18.0米,空机力臂16.25米。总力矩除以总重量得到重心位置,再转换成MAC百分比,安全范围是25%到38%。Bal

整体设计比前两次规范,字段都是private,类之间的职责划分比较清楚。但有一个明显的问题:Storage类的items字段用的是原始ArrayList,没有指定泛型类型,导致每次取元素都要强制类型转换成Product。这种写法容易引发类型转换异常,代码也不够优雅。另外Sorter类里又是手写的冒泡排序,效率不高。

BalanceCalc里的getCenter方法做了除零判断,这个细节处理得不错,避免了程序在边界条件下崩溃。

beb5f6a061a378dbdfaa2064020231a2
988d323b2d5e5dd0d9ef9ba0c41c3802

第三次作业圈复杂度分布不太均匀,BalanceCalc和Sorter的复杂度很低,但Main类的圈复杂度达到了12左右,主要原因是输入校验和条件分支太多。AirTrip类的方法数量偏多,与其他类的耦合度较高。

三、采坑心得

一:nextInt和nextLine混用导致输入被跳过

第一次作业就栽在这个问题上。我写的是先用nextInt读整数,再用nextLine读字符串,结果字符串读出来是空串,程序直接往下执行了。

原因分析:nextInt只读取数字字符,不消费末尾的换行符。紧接着调用nextLine,它会读取当前行的剩余内容,也就是那个换行符,所以直接返回空串。

解决方案有两种:一是在nextInt后面加一个nextLine把换行符消耗掉,二是全部用nextLine读整行再手动解析。第二次作业开始我就注意这个问题了,遇到数字和字符串混读的场景都会处理一下。

二:浮点数直接比较导致判断失准
第二次作业判断货舱剩余容量时,我写的是当前重量加上货物重量小于等于最大载重就允许装入。看起来没问题,但浮点数在计算机中采用IEEE754标准表示,很多十进制小数无法精确存储。例如0.1加0.2的结果并不是精确的0.3,而是一个非常接近的值。这导致边界条件判断时出现偏差——刚好装满的情况可能被判定为超载。

第三次作业的代码里用了大于最大载重加0.001这种写法,引入了一个容差阈值。这是常见的工程做法,我也学到了浮点数比较不能直接用等号或小于等于,要留一个允许的误差范围。

三:ArrayList只声明没初始化,空指针异常
写第二次作业的时候,我在类里声明了两个List类型的变量,一个存位置,一个存货舱货物。然后在构造方法里直接调用add方法,运行时抛出空指针异常。调试了半天才发现,这两个变量只是声明了引用,并没有指向实际的ArrayList对象,默认是null。

正确的写法是在构造方法中用new关键字创建ArrayList对象然后赋值给这两个变量。Java不像某些语言会自动初始化容器类型,必须显式地用new来分配内存。这个错误虽然低级,但初学时确实容易忽略。

四:手写排序的比较条件写反了

第二次作业要求货物按重量降序排列,重量大的排在前面优先装。我写的冒泡排序比较条件一开始写成了升序的逻辑,结果小重量的货物先被处理,导致大重量的货物后面装不进去。

冒泡排序的核心是相邻元素比较,如果前一个小于后一个就交换,这样大的元素会逐步“冒泡”到序列前面。我把条件写反成前一个大于后一个才交换,结果小的跑到了前面。这种手写排序很容易出细节错误,后来我改用Collections.sort配合Comparator,代码更可靠。

五:输入负数没做合法性校验

第三次作业题目明确要求输入不能为负数。我第一次提交的时候没做校验,用户输入负数时程序照常运行,算出来的重量是负数,力矩也是负数,重心位置完全错误。

后来补上了校验逻辑,前货舱的行数列数最大载重、后货舱的行数列数最大载重、旅客数量、行李重量、货物编号、前后货舱货物数量都加了类似的边界检查,只要任何一个数值小于0就直接输出错误提示并返回。这个教训让我意识到,写代码不能默认用户输入都是合法的,防御性编程是必要的。

四、改进建议

下面只针对现有源码提修改意见,不增加新功能。

改进一:第一次作业的public字段改为private

第一次作业Cargo类和Flight类的字段都是public,外部可以直接读写,破坏了封装性。建议把字段改成private,然后添加对应的getter方法供外部获取。这样外部只能通过getter读取,不能直接修改内部状态。

改进二:第二次作业的排序改用Collections.sort

LoadDispatcher里手写的冒泡排序代码冗长且有出错风险,两重循环加上交换逻辑很容易写错。建议直接用Collections.sort方法,传入一个Comparator指定按重量降序排列,两行代码就能搞定,可读性也更好。

改进三:第三次作业的ArrayList加上泛型

Storage类的items字段使用的是原始ArrayList类型,没有指定泛型。建议改成ArrayList带Product泛型,这样编译器会进行类型检查,取出元素时也不需要强制类型转换,能避免运行时出现ClassCastException。

改进四:第二次作业的findCompartment改用HashMap

Flight类中的findCompartment方法通过for循环遍历货舱列表来查找目标货舱,每次查找的时间复杂度是O(n)。可以在Flight类里增加一个HashMap,键是货舱id,值是货舱对象,添加货舱时同时put进去,查找时直接用get方法,时间复杂度降到O(1)。

改进五:统一输入处理的风格

第一次作业混用了nextLine和nextDouble,容易出边界问题。建议统一使用nextLine读取整行,再用split方法分割成字符串数组,然后逐个解析成需要的类型。这样不用处理nextInt和nextLine之间的换行符问题,风格也统一。

五、总结

三次作业做下来,收获还是比较明显的。

第一次作业帮我建立了类和对象的基本概念,理解了如何定义类、实例化对象、使用List容器。Comparator排序也是这次学会的,虽然第二次作业又手写了一遍冒泡排序加深了理解。

第二次作业让我体会到了封装的意义,理解了为什么要把字段设成private。同时也学习了面向对象分析的思路,把现实世界中的货舱、航班、调度这些概念抽象成代码中的类。HashMap的使用也是这次作业中练习到的。

第三次作业涉及了物理计算与程序逻辑的结合,把力臂、力矩、重心这些工程概念用代码实现了。同时学会了输入合法性校验的必要性,防御性编程的习惯应该从早期就培养。多个类之间的协作设计也是这次作业的练习重点。

还需要进一步学习的方向:一是异常处理,目前的代码遇到非法输入就直接return退出了,应该学会用try-catch和自定义异常来更优雅地处理错误;二是设计模式,比如策略模式可以用来处理不同的排序策略;三是单元测试,现在的代码写完只能手动输入测试用例,应该学习JUnit框架来写自动化测试。

posted @ 2026-05-18 22:59  杨添爻  阅读(13)  评论(0)    收藏  举报