航空器配载与货运管理系统——三次作业总结

航空器配载与货运管理系统——三次作业总结

一、前言

1.1 三次作业概况

1.第一次作业是设计一个最基础的货运模块。输入一个航班号、最大载重,然后往里面加货物,按重量从高到低排序,最后算一下总重有没有超过最大载重。题量不大,大概就五六个类,代码一百多行。主要就是熟悉一下怎么把现实里的东西抽象成类,还有Scanner输入怎么处理。

2.第二次作业就开始上强度了。飞机不再只有一个大货舱,而是分成了好几个货舱(比如前舱、后舱),每个货舱有自己的最大载重,还有行列网格的位置。货物要先按重量排好序,然后一件一件往指定的货舱里塞,塞不进去就提示超载。同时还要算整个航班的总重,跟最大起飞重量和最大业载重量比一比。这次类变多了,有Position、Cargo、CargoCompartment、Flight、LoadDispatcher、InputValidator这些,得想清楚它们之间是啥关系。代码量大概两百多行。

3.第三次作业是最麻烦的一次。不光要装货,还要加上旅客和行李。每个旅客默认体重75kg,再加上行李重量。最关键的是要算重心——就是根据物理里的力矩公式,算出货物的重心位置,然后转换成%MAC(占平均空气动力弦的百分比),最后判断是不是在安全范围(25%到38%)里面。这次代码有9个类,要处理各种非法输入,比如负数、超出范围、货舱超载等等。

总的来说,难度确实是递增的。第一次作业我大概花了两三个小时就写完了,第二次作业搞了一整天,第三次作业前前后后折腾了五六天,主要是在调输入输出和那个重心计算的公式。

1.2 涉及知识点

这三次作业每一轮都在前面基础上加了新东西,涉及的知识点我梳理了一下:

1.Java基础语法方面:类的定义、构造方法、成员变量、访问器(getter)、toString()重写、静态方法、静态常量、final关键字。这些在三次作业里反复用到,算是巩固了基础。

2.面向对象设计方

类的封装(private属性+public方法)、单一职责原则(SRP,每个类只干一件事)、类之间的关系(组合、聚合、依赖)。第一次作业主要是组合和聚合,第二次加了依赖关系,第三次把各种关系混在了一起。

3.集合与数据结构方面ArrayList的增删改查、遍历(for循环和增强for循环)、列表元素交换(排序时用到)。三次作业都用ArrayList存货物、存旅客,算是把ArrayList玩熟了。

4.算法方面手工实现选择排序(第一次、第二次)和冒泡排序(第三次),按重量降序排列。题目不让用Collections.sort()和lambda,只能自己写双重循环。虽然算法本身不难,但边界条件(比如只有一个元素、所有元素相等)容易出错。

二、设计与分析

2.1 第一次作业:基础货物装载

2.1.1 题目要求

设计一个基础的航班货运配载模块,系统记录航班号、最大起飞重量、最大业载重量。地勤人员按货物重量从高到低添加货物,系统实时算总重量,判断超没超载。类设计必须符合单一职责原则(SRP),参考类图给了四个类:CargoSorter、LoadManifest、Cargo、Flight。

2.1.2. 代码规模

a6dcb0ab-dbce-48ed-ac51-4824524988a6

2.1.3 类图

图片12.1.4 复杂度分析

2.1.5 数据分析与心得

从数据上看,第一次作业整体复杂度很低,平均复杂度才1.50,说明大部分方法就是简单的getter/setter或者一两行逻辑。唯一复杂度达到5的方法是CargoSorter.sortByWeight(),这很正常——选择排序本身就是双重循环嵌套,外层循环找基准位置,内层循环找最大元素,再加上交换逻辑,块深度达到5层(for→for→if→if→赋值)。

LoadManifest.getTotalWeight()复杂度为2,因为它有一个单层for循环遍历货物列表累加重量。这个复杂度是合理的。

但有个数据让我挺不好意思的:注释比例0.0%。131行代码一行注释都没有,这习惯太差了。虽然当时觉得代码简单不用注释,但现在回头看,连sortByWeight()里那个选择排序的逻辑都没说明,别人看代码得琢磨半天。

块深度分布显示,5层深度的语句只有1个,就在排序方法里;4层深度的有5个,大部分集中在Main方法的输入处理和输出判断中。这说明第一次作业整体结构还是扁平的,没有过度嵌套。

2.2 第二次作业:多货舱分配

2.2.1 题目要求

第二次作业在第一次基础上扩展。飞机有多个货舱,每个货舱有唯一标识(比如"前舱")、最大载重、行数、列数(组成位置网格)。货舱创建时要内部生成Position列表,这是组合关系。货舱和货物是聚合关系,货物可以独立存在。

输入所有待装载货物(名称、重量、目标货舱ID),系统先按重量从高到低排序,再依次尝试装入指定货舱。如果目标货舱当前重量加上这件货物重量不超过最大载重,就装进去,否则提示失败。最后还要输出每个货舱的已装载总重量和状态,以及航班整体总重量跟最大起飞重量、最大业载重量对比。

2.2.2 代码规模

399fecba-9e5b-4953-9954-6081f440ea7f

2.2.3 类图

图片4

2.2.4 复杂度分析

2.2.5 设计心得

第二次作业代码量翻了一倍多(131行→273行),语句数从74涨到165,但平均复杂度只从1.50涨到1.56,控制得还行。这说明新增的功能虽然多,但我没有把所有逻辑堆在一个方法里,而是分散到了各个类中。

最复杂方法依然是排序:LoadDispatcher.sortCargos(),复杂度5。跟第一次作业的sortByWeight()一样,都是选择排序的双重循环。这说明只要我自己手写排序,复杂度5就是绕不开的坎。findCompartment()和findCargo()复杂度都是3,因为都是线性查找,一个for循环加一个if判断。

CargoCompartment的构造方法复杂度为3,比我想象的高。原因是我在构造方法里用双重循环生成所有Position对象(行循环嵌套列循环),还加了位置对象到列表里。这个逻辑其实可以抽成一个单独的initPositions()方法,降低构造方法的复杂度。

块深度分布有个明显变化:2层深度的语句从30个涨到了70个,成为占比最大的层级。这说明第二次作业的方法里开始出现更多的单层嵌套(比如for里面套if,或者if里面套方法调用),逻辑比第一次复杂了,但还没有出现大量深层嵌套。

2.3 第三次作业:重心平衡计算

2.3.1 题目要求回顾

第三次作业是最硬核的。在前两次基础上,引入旅客和行李。每个旅客总重 = 75kg标准体重 + 行李重量。关键是做载重平衡计算——算重心。

2.3.2 代码规模

2.3.3 类图

图片6

2.3.4 复杂度分析

2.3.5 设计中的问题

第三次作业的数据有个特别刺眼的地方:Main.main()的复杂度达到了24。这是什么概念?第一次和第二次作业最复杂的方法才5,这次直接翻了将近5倍。SourceMonitor显示这个方法有92条语句、44次方法调用、最大块深度4。

这说明我把太多事情塞进了main方法。让我回忆一下main里干了啥:读航班号、读前舱参数、读后舱参数、读旅客人数、循环读旅客行李、读货物总件数、读前舱货物件数、循环读前舱货物并装载、循环读后舱货物并装载、调用WeightBalanceCalculator生成报告、输出报告,才造成主方法的复杂度变得很高。

WeightBalanceCalculator.calculate()复杂度只有2,语句16条。这说明重心计算本身的逻辑并不复杂(就是顺序执行几个公式),但因为它被抽成了一个独立方法,所以复杂度控制得很好。这也反过来证明:把逻辑拆出去,复杂度就能降下来

LoadDispatcher.sort()复杂度4,比前两次的选择排序(复杂度5)还低一点。这是因为第三次用的是冒泡排序,虽然也是双重循环,但内层循环的上界是size-1-i,比选择排序的内层循环少了一个"找最大索引"的步骤,所以分支少一点。

块深度分布显示,2层深度的语句从第二次的70个暴涨到119个,占比超过一半。这说明第三次作业里大量出现了"for里面套if"或者"if里面套for"的结构。结合代码来看,主要是Main方法里的多层输入循环和装载判断造成的。

注释比例依然是0.0%。355行代码、235条语句、10个类,还是一行注释都没有。这个数据我自己看着都脸红,写的时候光顾着赶功能,完全没考虑以后回头看代码能不能看懂。

三、采坑心得

3.1 Scanner输入:nextLine()吃回车的问题

这三次作业,我至少在Scanner上栽了三次跟头。

  • 在第一次作业中我用sc.nextLine()读航班号,然后sc.nextDouble()读最大载重,然后sc.nextInt()读货物件数。问题出在哪儿?nextDouble()和nextInt()不会消耗掉行尾的回车符!所以后面如果跟sc.nextLine()读货物名称,第一次读到的会是空字符串!我的解决办法是在每次nextDouble()、nextInt()后面加一个:sc.nextLine();把这个残留的换行符吃掉。
  • 第二次作业输入格式变了,用的是next()读字符串,没这个问题。
  • 第三次作业又变回混用nextLine()和nextDouble()/nextInt(),我又栽了一次,后来才想起来加那个吃掉换行符的代码。
  • 我的建议是:要么全用next()系列(next(), nextInt(), nextDouble()),要么全用nextLine()然后自己parseInt/parseDouble。混用最容易出事。

3.2 排序算法:自己写选择排序和冒泡排序

三次作业都不让用Collections.sort()或者List.sort(),必须自己写循环。

  • 写排序的时候一定要注意:交换元素的时候,一定要用临时变量temp,不能直接cargos.set(i, cargos.get(j)); cargos.set(j, cargos.get(i));这样第二个set拿到的已经是被覆盖过的值了。

3.3 第三次作业的输入顺序:前舱货物件数p和总件数m

第三次作业的输入有个很绕的地方:先输入货物总件数m,再输入装载在前舱的货物件数p,然后输入p件前舱货物,最后输入m-p件后舱货物。

我一开始看题不仔细,以为是先输前舱货物数量,再输后舱货物数量。结果写代码的时候按这个理解去读,后舱货物全读错了。后来重新看题目才发现是"总件数m"然后"前舱件数p",后舱是"m-p"件。

3.4 非法输入处理:负数、超范围、货舱超载

第三次作业对有三种错误情况:

  • 第一种:负数。InputValidator里每个方法都要判断,如果读到的值小于0,打印"数值不能为负数!"然后退出。我一开始只在InputValidator里判断,但Main里直接sc.nextInt()的地方忘了包Validator,导致负数能溜进来。后来把Main里所有输入都改成走InputValidator的静态方法。
  • 第二种:超出范围。比如前舱货物件数p,必须在0到m之间。InputValidator有个nextIntInRange(sc, max)方法,如果输入大于max,就打印"输入必须在 0 到 X 之间!"然后退出。这个我测试的时候用p=-1、p=m+1都试了一遍,确保能拦住。
  • 第三种:货舱超载。这不是输入错误,是装载错误。前舱装货时,如果addCargo()返回false(超重),我一开始把警告信息写错了,把当前载重写成了剩余载重,后来对了一下题目要求才发现。还有就是System.exit(0)的位置,要在打印警告之后立刻退出,不能继续装后面的货。

3.5 前舱后舱的力臂判断

第三次作业里,前舱力臂是12.0m,后舱是22.0m。题目说"根据货舱ID值为1判断"是前舱,ID为2是后舱。我的CargoCompartment类里有id属性,在WeightBalanceCalculator里判断:这里我一开始想复杂了,想着要不要用字符串判断"front"、"rear",后来看题目明确说用ID值1和2判断,就简单了。但这也说明读题一定要仔细,别自己脑补需求。

3.6 组合关系:Passenger必须在构造器里new Luggage

题目要求Passenger和Luggage是组合关系,"行李对象必须在Passenger构造器内部new出来,不对外暴露修改"。这样Luggage对象完全由Passenger控制,外面拿不到Luggage的引用,只能拿到总重量。这个设计其实挺合理的,就是刚开始写的时候忘了在构造器里new,写成了从外面传进来,后来看题目要求才发现要改。

四、改进建议

4.1 Scanner输入太恶心了,建议统一封装

三次作业都被Scanner的换行符坑过。我觉得可以写一个专门的InputReader类,把所有输入逻辑包起来,对外只提供readString()、readDouble()、readInt()这些方法,内部处理好换行符问题。这样Main里就不用到处写if (sc.hasNextLine()) sc.nextLine();

4.2 排序算法可以抽成更通用的

现在LoadDispatcher里的排序是写死的,只能排Cargo。如果以后要排别的(比如旅客按行李重量排),就得再写一个。建议搞个通用的排序工具,或者至少把比较逻辑抽出来。不过题目要求不能用lambda,那可以用策略模式搞个Comparator接口(虽然这次不让用interface,但以后可以用)。

4.3 WeightBalanceCalculator太长了,要拆分

现在generateLoadSheet()一个方法干了太多事:算旅客总重、算前舱后舱、算总重总力矩、算重心、格式化字符串、拼接报告。建议拆成几个小方法:calculatePassengerWeight(Flight flight)

calculateCompartmentMoment(CargoCompartment comp, double arm)

formatLoadSheet(...)

这样每个方法就十几行,好读好测。

4.4 错误处理别直接System.exit(0)

现在InputValidator一发现错误就System.exit(0),太粗暴了。建议改成抛出自定义异常(比如InvalidInputException),然后Main里统一catch,这样代码更灵活,也方便单元测试。直接exit的话,测试框架跑起来会直接挂掉。

4.5 Position类目前没用上,有点浪费

第二次作业要求CargoCompartment创建时生成Position列表,但我实际装载时并没有按位置分配。如果以后真要按位置装货(比如重货放底层),可以扩展addCargo(Cargo cargo, Position pos)方法。现在可以先留着,但最好加个TODO注释说明将来用途。

五、总结

5.1 学到了什么

这三次作业做下来,我实打实学到了不少东西:

  • 面向对象设计不是瞎分文件。一开始我觉得一个类就是一个文件,为了分而分。后来才明白,每个类要有明确的职责,比如Cargo只管存货物数据,CargoCompartment只管管货舱容量,WeightBalanceCalculator只管算重心。这样出了问题知道去哪找,不会在一个几百行的Main里翻半天。
  • 组合和聚合的区别。组合是"整体没了部分也没了"(比如Passenger没了,他的Luggage也没意义),聚合是"整体没了部分还在"(比如Flight没了,Cargo还是可以存在)。这次作业里货舱和位置是组合,货舱和货物是聚合,这些关系要在代码里体现出来(比如组合关系在构造器里new,聚合关系从外面传进来)。
  • 输入输出是门学问。以前觉得输入输出最简单,不就是Scanner嘛。这次被换行符、混用next和nextLine、格式化成一位小数这些细节折腾得够呛。现在我知道了,写程序之前一定要把输入格式在纸上画清楚,标清楚每一行是什么类型。
  • 测试不能光靠眼睛看。我一开始就是随便输几组数据看看输出对不对,后来发现有些边界情况(比如货物重量为0、旅客人数为0、刚好装满不超载)很容易漏。后来我就列了个测试清单,把正常情况和异常情况都跑一遍,这样心里才有底。

5.2 还需要学的东西

  1. 继承和多态。这次作业题目明确不让用,但以后肯定要用。我现在还不太清楚什么时候该用extends,什么时候该用interface。比如前舱和后舱现在是用同一个CargoCompartment类,靠id区分,如果以后功能差异大了(比如前舱有温控,后舱没有),可能就需要继承一个Compartment基类。
  2. 异常处理。现在只会用System.exit(0),太糙了。要学try-catch、自定义异常、怎么让程序优雅地报错而不是直接崩掉。
  3. 单元测试。现在测试就是手动输入数据,效率太低。要学JUnit,给每个类写测试用例,特别是WeightBalanceCalculator这种计算类,可以写死输入输出,自动验证。
  4. 设计模式。现在代码里已经有点"工具类"的影子(InputValidator、WeightBalanceCalculator),但还不够系统。以后要学工厂模式(创建对象)、策略模式(换排序算法)、模板方法模式(不同机型的舱单格式)这些。

5.3 结语

三次作业是我学Java以来写过最完整的项目。从第一次的131行、平均复杂度1.50,到第三次的355行、平均复杂度1.97,最复杂方法从5暴涨到24,我切身体会到了"迭代开发"是什么意思——不是每次都重写,而是在原来的基础上加东西、改东西。第一次打下的基础(Cargo类、排序逻辑)在第二次、第三次都复用了,这种"越写越省劲"的感觉挺爽的。

但数据也暴露了很多问题:Main方法成了上帝方法、注释比例永远是0、深层嵌套越来越多。这些问题以前写小作业的时候看不出来,现在代码量一上来就全冒出来了。好在现在是大一,还有时间慢慢改。希望下次作业能用到继承和多态,把这次写的"平铺直叙"的代码重构得更优雅一点。

posted @ 2026-05-18 21:11  CodeCatBatiao  阅读(11)  评论(0)    收藏  举报