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 题量与难度

度量指标V1V2V3
总行数 (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 类图展示:

image

 

2.12 整体设计思路:

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

2.13 分析图表:

下图是作业集一的 SourceMonitor 分析图表:

 

image

 

2.14 优缺点分析:

优点:

职责拆分清晰——Cargo 存数据、LoadManifest 管业务、CargoSorter 管算法、Flight.show()管输出,四个维度各司其职。当需要更换排序策略时只需修改 CargoSorter,更换输出格式时只需修改Flight.show(),变化被隔离在单一类内部;

采用 ArrayList 而非固定数组存储货物,避免了手动管理下标和扩容的繁琐;

缺点:

空指针保护不足。部分方法在操作集合前未校验参数是否为 null,后续作业中需要逐步建立起防御性编程的意识。

2.2 V2阶段:

2.21 类图展示:

image

 

2.22 整体设计思路:

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

2.23 分析图表:

下图是作业集二的 SourceMonitor 分析图表:

image

 

2.24 优缺点分析:

优点:

Position 与 CargoCompartment 的组合关系实现严格;

InputValidator 将校验逻辑从主流程中独立出来,使得 Main 中的输入处理代码更简洁;

缺点:

输出逻辑分散严重——CargoCompartment.addCargo() 内部有 System.out.printf,Flight.showall() 和 CargoCompartment.show() 也各自输出,导致输出格式一旦需要调整,需要修改多个类中的多处代码,违背了"展示逻辑集中管理"的原则;

Flight.addCargo() 中货舱ID匹配采用线性遍历,当货舱数量较大时效率偏低,且未匹配到时静默跳过,缺少异常反馈;

2.3 V3阶段:

2.31 类图展示:

image

 

 

2.32 整体设计思路:

加入了旅客(Passenger)和行李(Luggage),还要算复杂的力矩和重心(CG% MAC),而且有严格的限制条件(不让使用继承与多态),这就导致为了代码的简洁与效率,我必须重新设计各个类之间的关系,重新进行了解耦,按照题目要求实现了下面的效果。

  • 严密的封装:旅客携带行李,行李绝对不能脱离旅客单独存在。
  • 计算与业务分离:把所有航空物理学公式,全部扔进了一个叫 WeightBalance_Calculator 的静态工具类里。

这次重构也让我体会到了单一职责的爽点,Flight 类不用自己去算重心,它只负责“我有多少人、多少货”,然后把它自己当作参数传给WeightBalance_Calculator.getFlightCGMAC即可。 这种设计不仅把 Flight 类的负担减到了最轻,也让整个系统的可读性大幅提升。这也解释了为什么 V3 的业务最繁重,但代码逻辑却比 V2 显得更加清晰。

2.33 分析图标:

下图是作业集三的 SourceMonitor 分析图表:

image

 

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。这带来两个问题:

  1. 调用方失去控制权
  2. 不可测试,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:感悟

编程能力的提升从来不是线性的,而是在反复的"犯错→修正→理解→内化"中跳跃前进。三次作业做完了,但面向对象设计的学习才刚刚开始。后续我将继续保持这种"做完之后回头审视"的习惯——多追问自己一句"还能不能更好?"、多重构一次旧代码、多写一篇总结——让每一次作业不只是拿到一个分数,而是变成自己技术成长的实实在在的台阶。

posted @ 2026-05-18 00:48  ry7oll  阅读(15)  评论(0)    收藏  举报