面向对象程序设计前三次题目集总结
一、前言
整体来说,这三次题目集是一个真实业务场景的层层递进过程,知识点从语法层面一路提升到建模层面:
- 从基础的单货舱装配、计算总重量,到多货舱装配、计算整机超载,再到加入旅客、行李、计算力矩和重心、配平安全;
- 知识点从Scanner 陷阱、排序,到组合/聚合、List、稳定排序,再到物理公式建模、静态常量、纯静态工具类,难度稳步提升。
- 类的数量也从5个到10个;
- 代码量从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.第一次题目集
类图:

类级度量:
| 类 | 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 |
方法复杂度:

第一次题目集也是第一次用Java完成一个大型的综合实践项目,根据题目的要求,一共设计了七个类,对应6个职责,每个类都对应一个职责:
- 实体职责(Cargo / Flight):
Cargo类和Flight类只负责存储数据和输出数据,不用进行任何业务的判断; - 输入职责(ConsoleLoader):
ConsoleLoader类负责把控制台输入的文本解析成对象,同样不用进行任何业务的判断; - 业务职责(LoadManifest):
LoadManifest类负责进行装载、累加总重和超载判定,是唯一负责判断装载是否符合规则的判定者; - 算法职责(CargoSorter):
CargoSorter类负责排序独立成类。这个决定后来被证明非常主要——当我不断提交显示测试点4失败时,每次修改我都只需要修改这个类,其他六个类一点都不用动; - 输出职责(ManifestPrinter):
ManifestPrinter类只负责对输出进行格式化,从v(G)=3可以看出来,是全系统最简单的类之一; - 流程职责(Main):
Main类的v(G)=1,足以说明其流程控制没有负责任何业务。
从题目中获得的心得:
1.首先就是根据面向对象设计的要求,要把排序从LoadManifest拆出来,这样每次换算法都只需要一次替换,很好得体现了什么是面向对象编程;
2.很反直觉的是,整个系统分支最多的不是业务类而是输入解析类(ConsoleLoader 分支 8 个,与 CargoSorter 并列最高),因为输入的排版真的是千奇百怪,各种各样的都有,全都是挖的坑。
2.第二次题目集
类图:

类级度量:
| 类 | 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 |
方法复杂度:

第二次题目集的主要考察点其实就是整体和部分的关系的区分:
- 组合关系:
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.第三次题目集
类图:

类级度量:
| 类 | 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 |
方法复杂度:

第三次题目集难度更上一层楼,也多了各种各样、莫名其妙地要求。该题目的三个设计约束都落在了类图上:
1.WeightBalanceCalculator 没有持有任何Flight类的成员变量,Flight 只作为参数传入。
2.Passenger构造器内部的new Luggage,因为行李对象不能外暴露,所以旅客必带行李这一业务直接被固化在构造器里;
3. LoadDispatcher.sortCargos被要求必须使用纯循环冒泡(前面又不让用冒泡,现在又必须要用)。
关于复杂度:generateLoadSheet的v(G)=10、main的v(G)=6。说实话确实有点高了。generateLoadSheet一个人干了三个活:"排序调用 + 四步公式计算 + 舱单打印",这样严格来说并没有把面向对象编程做得很彻底,因为计算与输出仍然是耦合的。
这次题目集的心得:
1."依赖"这个东西让工具类变得可测:构造一个`Flight 对象就可以离线验证配平公式,不需要真的从控制台喂输入;
2.复杂的计算公式要一步步执行,就比如题目中的物理公式要分五步实现(旅客力矩 → 货舱力矩 → 全机汇总 → 实际重心 → %MAC),每步都要单独设计一个局部变量。
三、踩坑和心得
下面的每个坑,都是我提交代码测出来的
- 第一个坑:第一次题目中的排序算法,要使用特定的算法,不能用冒泡排序
如果选择冒泡排序,或者稳定排序,当货物重量相同,但是名字不同,会默认按输入顺序排序,就会导致测试点4不通过;经过一次次替换排序算法后,最终答案竟然是选择排序,最后输出时,会颠倒货物重量相同,但是名字不同的货物顺序,看起来就很反常识,但是没办法,正确答案就是这样。
最后通过新增CargoSorter手写选择排序,解决了测试点4不通过。 - 第二个坑:第二次题目里老师给出的输出格式里的 "(超载)" 是注释不是题目要求的输出
这个是最离谱的一个坑了,谁能想到括号里的是注释,不是题目最后输出时的要求。只能说吃一堑长一智了。 - 第三个坑:第三次题目里P的要求范围明明写了0≤p≤m,但是在测试输出时却显示0<p<m,也就是说,p的最小值不是0,而是1
题目和实际的测试要求不符合,这也是没招了,只能在提交代码之后看到错误才能修改,也只能跟着测试要求改。 - 第四个坑:第三次题目集,最后打印舱单展示时要按照货物id顺序展示,不能按照装载顺序展示
这个坑也不能算得上坑,只是按照货物id顺序展示更美观,只能说要求是这样。
四、改进建议
- 对于第一次题目集,没有太大的问题,主要是注释太少了,需要在编写代码时同步编写注释,在刚写完的时候可以在没有注释的情况下看懂,但是一旦时间太长,在没有注释的情况下去回看就容易看不懂了。
- 对于第二次题目集:
要拆掉v(G)=10print:按输出段落拆成printCargoLines(货物装载) /printCompartmentStats(货舱统计)/printFlightSummary(整机比对)三个私有方法,而主方法只做编排,这是第二次题目集最明确的技术债; - 对于第三次题目集,因为第三次题目集的问题最多。
需要做的就是要拆开"计算"与"打印",也就是把generateLoadSheet拆为两个部分:
第一部分LoadSheetResult calculate(Flight)只负责纯计算,返回总重/总力矩/CG/%MAC/评估;
第二部分print(LoadSheetResult)只负责纯输出。
通过这一步,能把原本v(G)=10拆成两个v(G)≤5的方法,并让公式可以脱离控制台做单元验证,进一步落实面向对象编程的要求;
五、总结
首先就是学到了什么:
- 从语法层来说:Scanner的行式与next()/nextDouble()按空白符读式读取的本质差异;%.1f 与浮点舍入;EOF 行为。
- 从设计层来说:面向对象编程从口号变成可操作判断——"改动一个东西是否只动一个类";组合/聚合/关联/依赖四种关系的构造器级区别。
- 从算法层来说:排序稳定性的工程意义(V1使用选择排序、V2使用稳定排序、V3则使用手写冒泡,三次题目集三种考法);浮点求和顺序对边界判断的影响。
- 从方法层来说:失败定位的完整路径——从"盲试"到"利用唯一差异点推理(只有失败用例才出现的输出行)"再到"检索外部证据交叉验证";这条方法论比任何单个知识点都值钱。
其次还需要进一步学习的地方:
- 数值计算:一些公式和方法的利用、浮点比较的容差设计——本轮只在"位级复现参考实现"上做到防御,缺乏系统性认知;
- 测试工程:使用一些满足题目要求的输入和不满足要求的输入,测试代码是否满足要求,不能只靠提交代码后才知道哪里不对;
- 建模工具:老师推荐的这个几个工具:PowerDesigner工具的利用、SourceMonitor工具指标的官方定义;
对课程与作业组织的一点小建议
- 测试点要透明:代码提交给PTA,失败的测试点不显示到底输入了什么,只能一个个猜。至少给一个失败用例输入规模和特征描述。
- 减少题目要求和实际要求的歧义:比如(超载)竟然是注释,题目里说p最小值是0但是测试里最小值是1,建议在题面用一句"注意"明示一下——要考察的是细心而非能力;
浙公网安备 33010602011771号