面向对象程序设计前三次题目集总结

一、前言
整体来说,这三次题目集是一个真实业务场景的层层递进过程,知识点从语法层面一路提升到建模层面:

  1. 从基础的单货舱装配、计算总重量,到多货舱装配、计算整机超载,再到加入旅客、行李、计算力矩和重心、配平安全;
  2. 知识点从Scanner 陷阱、排序,到组合/聚合、List、稳定排序,再到物理公式建模、静态常量、纯静态工具类,难度稳步提升。
  3. 类的数量也从5个到10个;
  4. 代码量从100多行到400多行。

三次叠加正好覆盖了面向对象课程的核心考察面——单一职责、组合/聚合/依赖三种关系、手写排序、以及"能跑"到"完全符合规格"之间的那段距离。

二、设计与分析

  • LOC:物理行(含注释与空行);语句数:以 ; 结尾的代码行;分支数:if/for/while/case 与 &&/||/? 的出现次数;
  • 方法复杂度:v(G) 圈复杂度 = 1 + 决策点数(决策点:if/for/while/case/catch 与 &&/||/?);iv(G) 设计复杂度 = 1 + 含方法调用的决策点数(衡量决策对类间交互的影响);ev(G) 基本复杂度 = 1 + 嵌套在控制结构内部的决策点数 + 多余出口(衡量不可化简的结构混乱度)。报表中红色为超出告警阈值(ev≥4、iv/v≥10);

1.第一次题目集

类图:
image

类级度量:

类 LOC 语句 分支 方法数 最复杂方法 v(G)
Cargo 17 6 0 3 构造器 (1)
Flight 17 5 0 3 构造器 (1)
CargoSorter 19 6 3 1 sortByWeightDesc (4)
LoadManifest 48 12 4 8 addCargos (3)
ManifestPrinter 10 3 2 1 print (3)
ConsoleLoader 44 14 8 5 readCargos (4)
Main 19 9 0 1 main (1)
合计/最大 174(类体) 55 17 22 4

方法复杂度:

图4 V1方法复杂度报表(SourceMonitor 口径)

第一次题目集也是第一次用Java完成一个大型的综合实践项目,根据题目的要求,一共设计了七个类,对应6个职责,每个类都对应一个职责:

  1. 实体职责(Cargo / Flight):Cargo类和Flight类只负责存储数据和输出数据,不用进行任何业务的判断;
  2. 输入职责(ConsoleLoader):ConsoleLoader类负责把控制台输入的文本解析成对象,同样不用进行任何业务的判断;
  3. 业务职责(LoadManifest):LoadManifest类负责进行装载、累加总重和超载判定,是唯一负责判断装载是否符合规则的判定者;
  4. 算法职责(CargoSorter):CargoSorter类负责排序独立成类。这个决定后来被证明非常主要——当我不断提交显示测试点4失败时,每次修改我都只需要修改这个类,其他六个类一点都不用动;
  5. 输出职责(ManifestPrinter):ManifestPrinter类只负责对输出进行格式化,从v(G)=3可以看出来,是全系统最简单的类之一;
  6. 流程职责(Main):Main类的v(G)=1,足以说明其流程控制没有负责任何业务。

从题目中获得的心得:
1.首先就是根据面向对象设计的要求,要把排序从LoadManifest拆出来,这样每次换算法都只需要一次替换,很好得体现了什么是面向对象编程;
2.很反直觉的是,整个系统分支最多的不是业务类而是输入解析类(ConsoleLoader 分支 8 个,与 CargoSorter 并列最高),因为输入的排版真的是千奇百怪,各种各样的都有,全都是挖的坑。

2.第二次题目集

类图:
image

类级度量:

类 LOC 语句 分支 方法数 最复杂方法 v(G)
Position 30 10 0 6 构造器 (1)
Cargo 23 7 0 4 构造器 (1)
CargoCompartment 82 28 8 10 addCargo (4)
Flight 53 15 4 8 findCompartment (3)
LoadDispatcher 26 5 3 3 findCargo (3)
InputValidator 50 12 12 4 readIntInRange (5)
LoadReportPrinter 39 13 9 1 print (10)
Main 43 23 7 1 main (8)
合计/最大 346(类体) 113 43 37 10

方法复杂度:
图5 V2方法复杂度报表(SourceMonitor 口径)

第二次题目集的主要考察点其实就是整体和部分的关系的区分:

  • 组合关系:CargoCompartment 构造器内部按 rows×cols 生成 Position 列表,货物的位置和货舱同时存在和消失;
  • 聚合关系:cargos: List<Cargo> 里面的货物在装载之前就存在,只是到底装入哪个舱是在实际装载的时候才决定;

对于方法的复杂度:LoadReportPrinter.print 仅这一个方法的 v(G)就=10(9 个决策点),是全系统复杂度最高的方法,其决策点整整占全部决策点的 21%(9/43)。主要是因为它干的活最多,一个人就把"装载明细、舱统计、整机对比、四种超载状态"全部干完。第一次题目提交的时候能跑对,但这是一个隐藏的定时炸弹——它导致我第三次题目不得不重写整个输出层,因为这一层从来就没有被真正拆开。

从这次题目集中获得的心得:
1.InputValidator整个的类分支一共12个、readIntInRange的v(G)=5,再次说明校验逻辑是分支大户,需要做的事情最多,最容易踩坑,也最容易导致代码出现问题;
2.与第一次题目集相比,Main的v(G)从1涨到了8,主要是因为多货舱的输入流程开始变复杂,导致流程层也开始变复杂。

3.第三次题目集

类图:
image

类级度量:

类 LOC 语句 分支 方法数 最复杂方法 v(G)
Position 22 8 0 4 构造器 (1)
Cargo 17 5 0 3 构造器 (1)
Luggage 11 3 0 2 构造器 (1)
Passenger 20 7 0 3 构造器 (1)
CargoCompartment 68 23 6 7 构造器 (3)
Flight 33 8 2 6 addPassenger (2)
LoadDispatcher 25 5 5 2 sortCargos (4)
InputValidator 40 16 6 3 readInt (3)
WeightBalanceCalculator 83 36 9 1 generateLoadSheet (10)
Main 57 28 5 1 main (6)
合计/最大 376(类体) 139 33 32 10

方法复杂度:

图6 V3方法复杂度报表(SourceMonitor 口径)

第三次题目集难度更上一层楼,也多了各种各样、莫名其妙地要求。该题目的三个设计约束都落在了类图上:
1.WeightBalanceCalculator 没有持有任何Flight类的成员变量,Flight 只作为参数传入。
2.Passenger构造器内部的new Luggage,因为行李对象不能外暴露,所以旅客必带行李这一业务直接被固化在构造器里;
3. LoadDispatcher.sortCargos被要求必须使用纯循环冒泡(前面又不让用冒泡,现在又必须要用)。

关于复杂度:generateLoadSheet的v(G)=10、main的v(G)=6。说实话确实有点高了。generateLoadSheet一个人干了三个活:"排序调用 + 四步公式计算 + 舱单打印",这样严格来说并没有把面向对象编程做得很彻底,因为计算与输出仍然是耦合的。

这次题目集的心得:
1."依赖"这个东西让工具类变得可测:构造一个`Flight 对象就可以离线验证配平公式,不需要真的从控制台喂输入;
2.复杂的计算公式要一步步执行,就比如题目中的物理公式要分五步实现(旅客力矩 → 货舱力矩 → 全机汇总 → 实际重心 → %MAC),每步都要单独设计一个局部变量。

三、踩坑和心得
下面的每个坑,都是我提交代码测出来的

  1. 第一个坑:第一次题目中的排序算法,要使用特定的算法,不能用冒泡排序
    如果选择冒泡排序,或者稳定排序,当货物重量相同,但是名字不同,会默认按输入顺序排序,就会导致测试点4不通过;经过一次次替换排序算法后,最终答案竟然是选择排序,最后输出时,会颠倒货物重量相同,但是名字不同的货物顺序,看起来就很反常识,但是没办法,正确答案就是这样。
    最后通过新增CargoSorter手写选择排序,解决了测试点4不通过。
  2. 第二个坑:第二次题目里老师给出的输出格式里的 "(超载)" 是注释不是题目要求的输出
    这个是最离谱的一个坑了,谁能想到括号里的是注释,不是题目最后输出时的要求。只能说吃一堑长一智了。
  3. 第三个坑:第三次题目里P的要求范围明明写了0≤p≤m,但是在测试输出时却显示0<p<m,也就是说,p的最小值不是0,而是1
    题目和实际的测试要求不符合,这也是没招了,只能在提交代码之后看到错误才能修改,也只能跟着测试要求改。
  4. 第四个坑:第三次题目集,最后打印舱单展示时要按照货物id顺序展示,不能按照装载顺序展示
    这个坑也不能算得上坑,只是按照货物id顺序展示更美观,只能说要求是这样。

四、改进建议

  1. 对于第一次题目集,没有太大的问题,主要是注释太少了,需要在编写代码时同步编写注释,在刚写完的时候可以在没有注释的情况下看懂,但是一旦时间太长,在没有注释的情况下去回看就容易看不懂了。
  2. 对于第二次题目集:
    要拆掉v(G)=10 print:按输出段落拆成 printCargoLines(货物装载) / printCompartmentStats(货舱统计)/ printFlightSummary(整机比对)三个私有方法,而主方法只做编排,这是第二次题目集最明确的技术债;
  3. 对于第三次题目集,因为第三次题目集的问题最多。
    需要做的就是要拆开"计算"与"打印",也就是把generateLoadSheet拆为两个部分:
    第一部分LoadSheetResult calculate(Flight)只负责纯计算,返回总重/总力矩/CG/%MAC/评估;
    第二部分print(LoadSheetResult)只负责纯输出。
    通过这一步,能把原本v(G)=10拆成两个v(G)≤5的方法,并让公式可以脱离控制台做单元验证,进一步落实面向对象编程的要求;

五、总结
首先就是学到了什么:

  1. 从语法层来说:Scanner的行式与next()/nextDouble()按空白符读式读取的本质差异;%.1f 与浮点舍入;EOF 行为。
  2. 从设计层来说:面向对象编程从口号变成可操作判断——"改动一个东西是否只动一个类";组合/聚合/关联/依赖四种关系的构造器级区别。
  3. 从算法层来说:排序稳定性的工程意义(V1使用选择排序、V2使用稳定排序、V3则使用手写冒泡,三次题目集三种考法);浮点求和顺序对边界判断的影响。
  4. 从方法层来说:失败定位的完整路径——从"盲试"到"利用唯一差异点推理(只有失败用例才出现的输出行)"再到"检索外部证据交叉验证";这条方法论比任何单个知识点都值钱。

其次还需要进一步学习的地方:

  1. 数值计算:一些公式和方法的利用、浮点比较的容差设计——本轮只在"位级复现参考实现"上做到防御,缺乏系统性认知;
  2. 测试工程:使用一些满足题目要求的输入和不满足要求的输入,测试代码是否满足要求,不能只靠提交代码后才知道哪里不对;
  3. 建模工具:老师推荐的这个几个工具:PowerDesigner工具的利用、SourceMonitor工具指标的官方定义;

对课程与作业组织的一点小建议

  1. 测试点要透明:代码提交给PTA,失败的测试点不显示到底输入了什么,只能一个个猜。至少给一个失败用例输入规模和特征描述。
  2. 减少题目要求和实际要求的歧义:比如(超载)竟然是注释,题目里说p最小值是0但是测试里最小值是1,建议在题面用一句"注意"明示一下——要考察的是细心而非能力;
posted on 2026-11-10 21:07  未也  阅读(19)  评论(0)    收藏  举报