NCHU_25级Java面向对象课程作业集1-3总结
一、前言
我写这篇博客,主要是围绕着NCHU_航空器配载与货运管理系统的三次作业进行综合性总结。旨在对这几次作业中遇到的困难与自身的进步、不足进行回顾和总结,帮助自己更好地理解与学习面向对象的课程。
说实话,刚看到"NCHU_航空器配载与货运管理系统"这个题目时,我以为它只是一个披着航空业务外衣的普通练习题,但随着V1到V3三次作业的逐步迭代,我发现这不仅仅是一道练习题,而是一个在各种严苛约束下的小型工程。
这三次作业的迭代跨度非常大,难度也是肉眼可见地呈阶梯式递增:
- 作业集1(V1 基础配载):一切的起点。主要任务是把现实中的“航班”和“货物”抽象成对象,体会单一职责原则(SRP)。顺便熟悉一下
Scanner输入流和最基础的超载判定。当时写完觉得挺简单,但随着迭代的进行,事情开始不对了起来,越简单反而坑越多。 - 作业集2(V2 多舱调度):业务逻辑开始上强度。飞机不再只有一个大肚子,而是分成了前舱和后舱,甚至还要用二维的行列网格来模拟具体的货舱位置。在这里,我开始真正理解并实操了类与类之间的“组合”与“聚合”关系。
- 作业集3(V3 载重平衡):终极“大乱炖”。不仅加入了旅客和行李模型,更引入了真实的航空物理学计算——求各舱室的力臂、力矩,最后算出实际重心百分比(CG% MAC)。而且在多种设计限制下,必须对题目抽丝剥茧、深入理解需求,不断打磨代码架构,才能求得最优最简的实现。
1. 1 知识点覆盖
为了更加客观的阐述题目,在此附上表格进行说明:
| 作业 | 核心知识点 |
|---|---|
| V1 | 类与对象、数组操作、防输入吃回车、超值判定等 |
| V2 | 货舱行列模拟、多货舱调度、组合与聚合等 |
| V3 | 载重平衡计算、深层拆分、ArrayList等 |
1.2 题量与难度
| 度量指标 | V1 | V2 | V3 |
|---|---|---|---|
| 总行数 (Lines) | 105 | 233 | 367 |
| 类数量 (Classes) | 5 | 7 | 10 |
| 最大复杂度 (Max Complexity) | 5 | 9 | 6 |
| 平均复杂度 (Avg Complexity) | 2.13 | 2.50 | 1.94 |
二、设计与分析
在这个阶段,题目每迭代一次,我的类结构就要经历一次大换血。下面我将结合PowerDesigner 生成的类图等数据进行说明
2.1 V1阶段:
2.11 类图展示:

2.12 整体设计思路:
在V1中,业务场景相对简单(只有单个航班、纯货物装载)。按照题目要求,我设计了5个类,核心思想是实体与操作分离:Cargo 只管存货物的名称和重量,LoadManifest 负责管理货物集合,而排序这个动作,则单独抽离给了一个工具类 CargoSorter。
2.13 分析图表:
下图是作业集一的 SourceMonitor 分析图表:

2.14 优缺点分析:
优点:
职责拆分清晰——Cargo 存数据、LoadManifest 管业务、CargoSorter 管算法、Flight.show()管输出,四个维度各司其职。当需要更换排序策略时只需修改 CargoSorter,更换输出格式时只需修改Flight.show(),变化被隔离在单一类内部;
采用 ArrayList 而非固定数组存储货物,避免了手动管理下标和扩容的繁琐;
缺点:
空指针保护不足。部分方法在操作集合前未校验参数是否为 null,后续作业中需要逐步建立起防御性编程的意识。
2.2 V2阶段:
2.21 类图展示:

2.22 整体设计思路:
V2引入了多货舱和二维网格,最大的难点在于理清对象之间的生命周期和类之间的关系。在这里,我设计了CargoCompartmant(货舱类)和 Position(位置类)。货舱和位置是“同生共死”的组合关系,而货舱和货物则是“聚散离合”的聚合关系。为了统一处理越来越繁琐的输入,按照题目要求,引入了Inputvalidator 类。 但V2的设计也有明显缺陷——从SourceMonitor报表能看出来,V2 的最大圈复杂度飙到了 9,原因是我在LoadDispatcher(调度类)和 Main 方法里塞进了太多的 if-else 来判断“到底装前舱还是后舱”以及“是否超载起飞重量”,导致代码耦合度严重超标,这也为 V3 的重构埋下了伏笔。
2.23 分析图表:
下图是作业集二的 SourceMonitor 分析图表:

2.24 优缺点分析:
优点:
Position 与 CargoCompartment 的组合关系实现严格;
InputValidator 将校验逻辑从主流程中独立出来,使得 Main 中的输入处理代码更简洁;
缺点:
输出逻辑分散严重——CargoCompartment.addCargo() 内部有 System.out.printf,Flight.showall() 和 CargoCompartment.show() 也各自输出,导致输出格式一旦需要调整,需要修改多个类中的多处代码,违背了"展示逻辑集中管理"的原则;
Flight.addCargo() 中货舱ID匹配采用线性遍历,当货舱数量较大时效率偏低,且未匹配到时静默跳过,缺少异常反馈;
2.3 V3阶段:
2.31 类图展示:

2.32 整体设计思路:
加入了旅客(Passenger)和行李(Luggage),还要算复杂的力矩和重心(CG% MAC),而且有严格的限制条件(不让使用继承与多态),这就导致为了代码的简洁与效率,我必须重新设计各个类之间的关系,重新进行了解耦,按照题目要求实现了下面的效果。
- 严密的封装:旅客携带行李,行李绝对不能脱离旅客单独存在。
- 计算与业务分离:把所有航空物理学公式,全部扔进了一个叫
WeightBalance_Calculator的静态工具类里。
这次重构也让我体会到了单一职责的爽点,Flight 类不用自己去算重心,它只负责“我有多少人、多少货”,然后把它自己当作参数传给WeightBalance_Calculator.getFlightCGMAC即可。 这种设计不仅把 Flight 类的负担减到了最轻,也让整个系统的可读性大幅提升。这也解释了为什么 V3 的业务最繁重,但代码逻辑却比 V2 显得更加清晰。
2.33 分析图标:
下图是作业集三的 SourceMonitor 分析图表:

2.34 优缺点分析:
优点:
工具类思想运用得当——WeightBalanceCalculator 将所有计算公式封装为纯函数(输入 Flight,输出 double),不持有状态、不产生副作用。这种"计算与数据分离"的设计使得算法可以独立验证和复用;
方法拆分策略奏效——平均圈复杂度从V2的2.50降至V3的1.94,说明"拆大方法为多个小方法"在控制复杂度上是切实有效的;
缺点:
Flight类的职责过重——它同时管理货舱列表、旅客列表,并提供重量汇总、查找等辅助方法,成为了事实上的"上帝类";
InputValidator 中 System.exit(0) 的异常处理方式过于粗暴,直接终止JVM进程,调用方无法捕获并处理异常;
三、踩坑心得
3.1:排序算法选择陷阱:冒泡排序的"不稳定"问题
问题表现:
我最初使用冒泡排序对货物按重量降序排列,本地测试样例结果完全正确,但提交到PTA后多个测试点无法通过。经过反复排查,发现问题出在排序算法上——冒泡排序是稳定排序,而题目要求的输出顺序与不稳定排序(选择排序)的结果一致。
问题定位:
以下面这组数据为例说明两者的区别:
| 输入顺序 | 货物 | 重量 |
|---|---|---|
| 1 | A | 300 |
| 2 | B | 500 |
| 3 | C | 500 |
选择排序第1轮:maxIdx=0→1(B> A),不更新(C不大于B),交换B到位置0,A到位置1 → B(500)→A(300)→C(500)。第2轮:maxIdx=1→2(C> A),交换 → B(500)→C(500)→A(300)。
最终顺序:B→C→A(B在C前,保持了等重货物的输入顺序——但这只是巧合,选择排序本质上是不稳定的)。
解决方案:
在更复杂的数据排列下,二者的输出就会产生差异,而PTA的测试用例是基于选择排序的输出顺序设计的。将排序算法更换为选择排序后,全部测试点通过。
3.2:Scanner 输入获取时频繁触发 NoSuchElementException
在三版代码的迭代过程中,最频繁出现的运行时异常就是 java.util.NoSuchElementException。具体表现为程序启动后尚未完成全部输入读取就崩溃,错误堆栈指向 Scanner.nextDouble() 或Scanner.nextInt() 调用处。这个问题在V1到V3的早期版本中反复出现,尤其是在输入格式发生变化的过渡期。
问题定位:
nextLine() 与 nextXxx() 混用时换行符残留
int a = scanner.nextInt(); // 用户输入 "25\n",nextInt() 只消费 "25","\n" 残留在缓冲区 String s = scanner.nextLine(); // nextLine() 立即读到残留的 "\n",返回空字符串 ""
nextInt()、nextDouble() 等数值读取方法不会消费行尾的换行符,而紧随其后的 nextLine() 会把这个残留换行符当作一个空行读取,从而导致后续读取错位,抛出 NoSuchElementException。
解决方法:
在 nextXxx() 之后紧跟一个 nextLine(),清掉换行符。
3.3:浮点数精度丢失
问题表现:
在作业3的 WeightBalanceCalculator 中,重心百分比 CG%MAC 的计算结果偶尔会出现理论值与实际值的微小偏差。例如,当理论上CG%MAC 恰好等于安全上限 38.0% 时,程序实际计算出的值可能是 38.0000000001%。按照代码中的判断逻辑,38.0000000001 >38.0,于是被判定为"危险(RED)"——而物理上这个重心完全在安全范围内。
问题定位:
CG = 总力矩 / 总重量 // 除法①,可能产生无限小数 CG%MAC = ((CG - 15.0) / 5.0) × 100.0 // 减法、除法②、乘法
解决方案:
在判定时加入了一定的容错范围(1e-6),当理论值恰好卡在边界 25.0 或 38.0 上时,这个微小的误差就决定了"安全"与"危险"两种截然不同的输出。
3.4:无处不在的 NullPointerException
问题表现: 在 V2 和 V3 复杂的调度逻辑中,有时候为了寻找特定的货物,控制台会突然弹出一大串 NullPointerException(空指针异常)。
问题定位:
在LoadDispatcher 里写的 FindCargo 方法,如果遍历完 ArrayList 没找到对应的名字,方法最后会 return null;这是非常常规的写法。但是,主流程拿到这个返回值后,没有做非空校验,直接去点它的属性(比如cargo.getcargoWeight()),JVM 就会抛出空指针异常崩溃。
解决方案:
在调用查找方法时,写一个if(≠null)的判断。
四、改进建议
4.1:从"平铺式调用"走向分层设计
三次作业的代码结构是典型的"Main驱动一切"模式——Main 方法直接创建所有对象、调用所有方法,类之间虽然按SRP拆开了,但调用层次是平的。这在类数量达到10个的V3中已经开始显露问题:Main方法承担了流程编排、输入解析、对象创建、异常处理四重职责,代码行数膨胀,读起来需要频繁在不同抽象层级之间跳转。
建议将现有代码重构为四层结构:
| 分层 | 职责 | 现有类归属 |
|---|---|---|
| 数据层 | 封装核心数据模型,不包含业务逻辑 | Cargo、Position、Luggage、Passenger |
| 业务层 | 核心业务逻辑与调度 | Flight、CargoCompartment、LoadDispatcher、WeightBalanceCalculator |
| 校验层 | 输入校验与业务规则校验 | InputValidator |
| 展示层 | 统一输出格式 | Flight.show()、CargoCompartment.show() |
分层之后的好处是:每层的类只依赖下一层,不会出现展示层的类直接访问数据层的 “Cargo.cargoWeight" 这种跨层调用。
4.2:Flight 类的职责拆分
当前V3的 Flight 类同时承担了以下职责:
- 存储航班基本信息(航班号)
- 管理货舱列表(addCargoCompartment、getCargoCompartmants)
- 管理旅客列表(addPassenger、getPassengers)
- 提供数据汇总(getPassengerSum、getCargoComSum)
这使得 Flight 的参数列表、方法数量在三次迭代中持续膨胀。建议拆分为:
// 航班基本信息(变化频率低)
class FlightInfo {
private String flightNo;
// 未来可能扩展:起飞机场、目的机场、计划起飞时间等
}
// 装载信息(变化频率高,每次航班都不同)
class LoadInfo {
private List<CargoCompartment> compartments;
private List<Passenger> passengers;
public double getTotalCargoWeight() { /* 遍历货舱求和 */ }
public double getTotalPassengerWeight() { /* 遍历旅客求和 */ }
public double getTotalWeight() { /* 上述两者之和 */ }
}
// 重构后的Flight
class Flight {
private FlightInfo flightInfo;
private LoadInfo loadInfo;
// 仅保留航班级别的行为
}
增强了代码的可读性和可维护性。
4.3:不使用System.exit(0)
当前代码中,InputValidator.checkValid() 和 CargoCompartment.addCargo() 等深层方法在遇到异常输入时直接调用System.exit(0) 终止JVM。这带来两个问题:
- 调用方失去控制权
- 不可测试,System.exit(0) 会直接杀死测试进程。
后续可以使用 try-catch块,来对异常进行自定义处理。
4.4:注释优化
在SourceMonitor 分析图表中,我的代码注释率在V1V2V3中均为0.0%,这不仅反映我为了赶作业集“囫囵吞枣”,也反映了我日常写代码的差习惯与随意,经过这次题目集,我深刻反省到了自身在代码编写上的诸多问题,不论是题目拆分、架构设计,还是日常的代码注释、变量命名,都有很大的提升空间。
4.5:抽取重复的求和逻辑
V2的 Flight.getSum()、V3的 Flight.getCargoComSum() 和 Flight.getPassengerSum()本质上都是"遍历集合→累加某个字段→返回总和",只是操作的对象和字段不同。可以用一个通用的求和工具方法:
public static double sumOf(List<Double> values) {
double sum = 0;
for (double v : values) {
sum += v;
}
return sum;
}
降低重复,增强可读性。
五、总结:
5.1:成长与体会
回顾这三次作业的完整经历,从V1的基础货物排序输出,到V2的多货舱网格调度,再到V3整合旅客行李与重心平衡计算——每一次迭代都不是简单地在旧代码上"加功能",而是迫使我重新审视前一次的类设计是否还站得住脚。
V1的时候,我只关心"能不能输出正确结果"。到了V2,多货舱的引入让单货舱时期的设计假设全面失效——Cargo需要知道自己去哪、Flight需要管理多个舱、每个舱有自己的载重上限。V3更进一步,旅客和行李的组合关系、力矩和重心的物理计算、舱单的格式化输出, 每一个需求都在考验类设计的边界感。
做完这三次作业后,我能明显感受到自己从"面向过程的思维套了一个类的壳",过渡到真正用面向对象的视角去建模问题。最开始我只是机械地把 main 里的代码拆进不同的类,后来慢慢学会追问自己:这个类变化的理由有几个?这个关系是组合还是聚合?这个方法放在A类而不是B类的依据是什么?这种思维上的变化,比机械地写一百道题更有价值。
5.2:亟待补足的短板
拿了满分并不代表代码完美,反而暴露出了我目前的认知盲区:
- 异常处理(Exception Handling)的正规化:我必须改掉动不动就
System.exit(0)的恶习,学习如何自定义异常体系,掌握try-catch的正确实践,写出带有“韧性”的代码。 - 工程规范意识: SourceMonitor 报表里那 0.0% 的注释率是一个醒目的警示。未来我必须强迫自己养成写 Javadoc 文档注释的习惯,做到代码即文档。
- 设计模式(Design Patterns)的极度匮乏:面对复杂的业务流转,我目前只会用
if-else堆砌逻辑。下一步我必须系统学习《设计模式》,尤其是工厂模式和策略模式,学会如何优雅地解耦。 - 代码复用与抽象层次:V2和V3中存在不少"看起来很像但又不完全一样"的代码片段,比如多处遍历集合求和的循环。我目前的处理方式往往是复制然后微调,而不是抽象出一个通用的工具方法。这在当前两三百行的规模下还能接受,但当系统涨到几千行时,这种"复制即复用"的习惯会迅速让维护成本失控。后续需要刻意练习避免复制。
5.3:对教师与课程的建议
课上优秀代码展示:在每次作业提交截止后,建议老师能在课上挑一份规范的优秀代码(或者老师自己写的标准答案)展示一下。只有看到好代码长什么样,我们才知道自己那一坨 if-else 差在哪里。
测试样例与隐藏测试点的偏差可以更透明: PTA的样例输入/输出是我们唯一能参考的"正确答案格式”,但实际体验中,部分隐藏测试点的数据特征(如等重货物的排列顺序、边界值的处理方式)与样例之间存在不小的差异,导致"样例全对、提交零分"的情况时有发生。可以更加细化测试点,测试点的提示可以更加详细一点,越多越细的测试点,可以让学生更快理解到问题所在。
5.4:感悟
编程能力的提升从来不是线性的,而是在反复的"犯错→修正→理解→内化"中跳跃前进。三次作业做完了,但面向对象设计的学习才刚刚开始。后续我将继续保持这种"做完之后回头审视"的习惯——多追问自己一句"还能不能更好?"、多重构一次旧代码、多写一篇总结——让每一次作业不只是拿到一个分数,而是变成自己技术成长的实实在在的台阶。
浙公网安备 33010602011771号