一、 前言:当“能跑”遇上“优雅”,一场属于代码的重塑
如果要用一个词来形容这三次题目集的体验,我会选“撕裂感”。每一次迭代,都像是在已经勉强封顶的积木塔上继续加砖——你永远不知道下一秒是塔塌了,还是你找到了新的受力平衡点。很多时候,看着屏幕上那个“答案错误”的红色提示,心里其实很清楚,问题并不是出在某一行代码的语法上,而是出在自己对“架构”这两个字的理解还太浅薄。
这三次作业,其实就是一条非常清晰的进化链。最开始,我们只关心“货物能不能塞进去”,比的是谁的基础语法更熟练;后来,题目引入了多货舱与二维网格,我们开始被逼着思考“对象与对象之间到底是什么关系”;再往后,旅客、行李、力矩、重心百分比,以及极其苛刻的鲁棒性要求全部砸下来,这时候,如果还只是停留在“写方法”的层面,代码注定会崩盘。
知识点从“类与对象的初步映射”,一路蔓延到“单一职责原则的实践”、“组合与聚合的代码落地”、“稳定排序算法的打磨”,以及“面向真实业务场景的容错设计”。题量其实并不算大,但每道题的“厚度”很足——每一个需求点的背后,都藏着能够引发系统级连锁反应的陷阱。这篇总结,我想抛开那些冷冰冰的教科书式条目罗列,用三次作业中那些让我深夜抓狂、甚至不得不推倒重来的真实瞬间,聊聊我在面向对象这条路上,如何从一个“功能堆砌者”,慢慢长成一个“职责守护者”。
二、 设计与分析:源码里的“善”与“恶”,以及那些被降维打击的瞬间
2.1 第一次作业:被架空的排序器与“上帝类”的诱惑
第一次作业交上去的时候,成绩是全绿,但我心里其实很清楚,那份代码是有“原罪”的。
为了快速实现“按重量降序排列”,我在主方法里直接调用了一行内置的排序,还顺手用了 Lambda 表达式。排序结果当然是对的,但问题在于:我明明写了独立的排序类,里面甚至有完整的选择排序逻辑,可最后真正干活的却是主方法里那行“抄近道”的代码。排序类成了一个空壳,而主方法则悄悄膨胀成了一个既负责组装对象、又负责排序、还负责打印的“上帝类”。
当时的我觉得,既然题目没扣分,那就说明没问题,甚至还在心里暗自庆幸自己找到了捷径。但复盘时我才意识到:功能跑通不等于架构正确。那行内置排序就像一针麻醉剂,让我暂时忘记了“单一职责”这四个字的分量。如果后续迭代要求更换排序规则(比如重量相同时按名称字典序),或者禁止使用内置排序,这行代码就会成为最难拆的雷。好的设计,是让排序类真正“拥有”排序这个动作,而不是让它成为主方法里的一个摆设。
2.2 第二次作业:多货舱的博弈与满分设计的降维打击
第二次作业的滑铁卢来得非常直接:排序与超载测试,答案错误。
那一次我确实写了独立的排序类,也确实按照要求手动实现了选择排序。但当我把排序结果交给装载流程时,逻辑链条出现了裂缝。我的装载方法判断了“当前重量 + 新货物重量是否超出上限”,如果超出就跳过——但跳过了之后呢? 我的主流程没有接收这个“失败”的状态反馈,而是自顾自地继续执行,最终输出的结果与实际业务状态严重脱节。
后来我特意去翻了当时满分的实现逻辑,人家那个“装载方法”的设计,真的是四两拨千斤。它根本不搞什么花里胡哨的报错,而是在里面藏了一个很干脆的“红绿灯”机制:只要往里塞货物时,发现手里这件货加上之前已经装好的总重量,超过了舱位头顶的天花板,它甚至连个错都不报,直接丢给我一个“不行”的暗号(其实是返回了一个表示失败的布尔状态);只有当重量塞得下时,它才默默把货物收进去,并回我一个“行”的暗号。
就这么一个简单的动作,直接把“装货”和“汇报结果”这两件事给劈开了。主流程拿到这个暗号,再去决定是打印“成功”还是“超载失败”,这代码结构瞬间就清爽了,哪像我最开始那样,把判断和打印全揉在一个大杂烩里。
同时,满分实现在货舱类中完美体现了组合与聚合的区分:位置对象在货舱构造器内部批量生成,与货舱同生共死;而货物对象是从外部传入的引用,可以独立存在。这种设计让类图的线条干干净净,每个类的职责边界清晰得像是用尺子量过的。这也是我第一次体会到:布尔返回值是比抛异常更温柔、也更灵活的控制流工具。
2.3 第三次作业:物理力矩、鲁棒性溃败与全线归零的惨痛教训
第三次作业是三次里最惨烈的一次——多个核心测试点直接归零,其中包括配平评估的两个用例、前后舱容量不足的两个用例,以及输入范围非法的那个用例。看着屏幕上那一排零分,那种挫败感比第一次作业拿不到满分要强烈得多,因为我知道,这次的问题绝不是改个标点符号就能解决的。
先说说计算类的设计。 我在计算力矩时,把力臂值直接硬编码在判断逻辑里,靠货舱编号来决定用哪个力臂。这种写法在只有两个舱时勉强能活,但一旦扩展到多舱,代码瞬间变得臃肿且不可维护。
再说说最致命的输入流断裂问题。 我的主方法在读完前舱货物后,立刻判断了前舱是否超载。如果超载,我直接打印警告然后结束整个流程。表面上看逻辑没问题——超载了当然要停。但我忽略了一个致命事实:标准输入流里还排着后舱货物的数据没读呢。 这种提前退场导致数据流彻底错位,后续的统计和输出全部崩盘。满分实现的处理方式非常克制:它把超载判断下沉到货舱的装载方法内部,由装载方法返回布尔标志。主流程只有在收到“失败”信号时,才打印警告并终止程序。这样既保证了输入数据的完整性,又避免了业务逻辑与控制流的粗暴纠缠。
最后说说重心计算。 我一度以为,计算总力矩时应该用排好序的货物列表来累加——理由很简单:货物是按重量降序装载的,那计算顺序也应该一致。但实际上,力矩的累加与顺序完全无关。满分代码之所以在计算类里调用排序器,目的根本不是影响计算结果,而是为了输出格式——题目要求舱单明细按降序展示,所以它用一个副本排序,既满足了展示需求,又保留了原始数据的输入顺序。
对比维度 我的实现 满分实现 失分根源
超载终止时机 前舱读完即判断并退出 边读边装,装载失败时才终止 前后舱容量用例归零
输入越界处理 仅打印提示,未强制终止 打印提示后立即强制退出 输入范围用例归零
重心计算数据源 手动遍历货物列表 复用货舱当前重量并模拟排序 配平评估用例归零
排序算法落地 未在报告生成中强制使用冒泡排序 内部手写稳定冒泡排序 隐藏评分点失分
三、 采坑心得:那些被数据流和公式“教做人”的瞬间
3.1 输入流断裂:一个提前退场的动作引发的血案
这是第三次作业里最让我心痛的坑。当时前舱容量不足的测试点怎么都过不去,我反复检查了容量判断公式,确认“当前重量加上新货物重量大于上限”这个逻辑绝对正确。打印警告的文案也一字不差。那为什么还是零分?我甚至一度怀疑是测试点数据有问题。
后来我对着输入流想了很久,终于想明白了:问题出在“什么时候退场”。我的代码在读完前舱货物后立刻判断是否超载,如果超载就结束。但测试数据里,后舱的货物信息还在输入缓冲里等着被读取。我的提前退场让这些数据永远留在了缓冲区,后续读取发生错位,输出自然全乱。
反思: 配载是一个严格的流水线——输入、装载、计算、输出,必须按序推进。正确的做法是把超载判断下沉到装载动作内部,由装载方法返回布尔标志,主流程根据这个标志决定是继续读取还是终止。这样既保证了数据读取的完整性,又避免了控制流的粗暴截断。这件事让我明白,在流式处理中,“优雅地失败”比“提前地退出”重要得多。
3.2 排序的“伪相关”:重心计算根本不在乎顺序
在计算重心百分比时,我一度陷入了一个逻辑怪圈:既然货物是按重量降序装载的,那计算总力矩时也应该按降序累加。我甚至在代码里手动对货物列表做了一次排序,结果和预期值差了 0.1% 左右。
后来逐行对比满分实现,我才发现自己完全理解错了排序的用途。力矩的计算与累加顺序无关,它只与总重量和力臂的乘积有关。满分代码在计算类里调用排序器,目的只有一个:生成舱单时按降序输出明细。为了不破坏原始数据的插入顺序,它用创建一个全新副本的方式来进行排序。
这个细节让我意识到:排序不是为了计算,而是为了展示。把这两件事混在一起,就是我逻辑混乱的根源。而且在业务代码中,保护原始数据的顺序不因排序而丢失,本身也是一种非常严谨的工程习惯。
3.3 数值精度的陷阱:标准体重加在行李前面还是后面
测试配平评估的两个用例时,我算出的重心百分比总是差那么零点几个百分点。我一度怀疑是浮点数精度问题,把变量类型从 float 改成 double,依然无效。
后来逐行推演公式才发现,我在计算旅客总重时,把标准体重 75.0kg 加在了行李重量之后。虽然数学上加法满足交换律,但在代码执行路径上,这个顺序差异暴露了一个更深层的问题:我没有严格遵循题目描述的计算步骤。题目原文写的是“旅客总重 = 标准体重 + 行李重量”,这个顺序不仅是一个数学表达式,更是业务逻辑的精确映射。
从那以后,我养成了一个习惯:业务公式的代码实现,必须严格遵循题目的文字描述顺序,哪怕数学上等价,也不凭感觉“优化”。航空配载这种场景,容不得半点自作聪明。
四、 改进建议:从“通过测试”走向“经得起推敲”
4.1 用配置化替代硬编码力臂
现在的计算类靠货舱编号来判断力臂值,这种写法在只有两个舱时还能凑合,但一旦业务扩展到五个舱,代码就会变成一长串条件分支。更好的做法是在货舱类中增加一个力臂属性,在构造时从输入或配置中读取。计算总力矩时直接遍历所有货舱,用“当前重量 × 力臂”累加。这样无论增加多少个货舱,计算类都不需要改动一行代码。这种“对扩展开放,对修改关闭”的设计,才是应对需求变化的正确姿势。
4.2 把输入校验抽象成独立的拦截器
目前的校验类虽然集中了校验逻辑,但与控制台输入紧密耦合。一旦输入源发生变化(比如从控制台改为文件读取),就需要大改。可以设计一个校验结果类,包含“是否合法”和“错误信息”两个字段。校验类不再直接终止程序,而是返回这个结果对象,由主流程决定是继续还是终止。这样将来迁移到 Web 环境时,可以通过捕获异常来替代强制退出,代码的可移植性会大幅提升。防守的动作可以是一样的,但防守的方式应该尽量灵活。
4.3 对稳定排序的深度打磨
说句实话,这作业里关于“冒泡排序”的那点折磨,纯粹是因为我手欠。题目明明要求同等重量的货物得按输入先后顺序排,我那时候在写冒泡排序的两两交换条件时,脑抽写了“只要左边小于等于右边就换位置”。结果倒好,两件一样重的货,明明人家先进来的,愣是被我换到后面去了,这就直接导致那个排序测试点当场暴毙。
后来我怎么改的?其实改动小得可怜。就是把那个判断条件里的“等于号”给掐了。变成“只有左边严格小于右边的时候,我才允许它俩换位置”。就这么一个微不足道的细节,瞬间把“稳定性”给保住了。这件事让我明白,算法能不能跑出正确结果是一回事,但能不能在那些重量一模一样的货物里,死死记住人家进门的先后顺序,才是真功夫。
五、 总结:代码之外,我学到了什么
这三次作业,代码量不算夸张,但每一次迭代都在逼我重新审视自己的设计习惯。回过头看,我在这三次作业里到底捞到了啥?
老实说,最大的收获不是写了多少行代码,而是终于懂得了“各司其职”有多省心。以前我总是习惯性地把找数据、算重量、拼接输出的活儿全扔进主函数里,觉得顺手。但这次扩展多货舱之后,我才真真切切地感觉到,如果有专门负责排队的、专门负责算数学题的、专门负责盯着输入数据的类来分担这些活,那整个系统哪怕加塞儿再多新需求,我也能稳住阵脚。哪个地方要改,直奔那个对应的类就行,再也不用去翻几百行的主方法里大海捞针了。
另外,算是把组合跟聚合的区别给吃透了。你看那个货舱里的位置格子,我一创建货舱,这些格子的坐标就在它的地盘里生出来了,货舱要是没了,格子也就灰飞烟灭了,这就是“组合”。而我把货物塞进货舱时,只是把外面已经包装好的货物引用传了进去,哪怕货舱卸空了,货物本身还在,这就是“聚合”。写在代码里就是构造器里怎么安排、参数里怎么传的区别,真到了画类图的时候,一看关系线就清清楚楚。以前看书上的定义总觉得抽象,现在自己动手写一遍,脑子里那个概念才算真正落地。
还有一点特别深的体会,就是面对物理公式千万不能抖机灵。算飞机重心的时候,我觉得标准体重加到行李后头逻辑通顺,结果算出来的百分比死活差那么零点几,死活过不去测试点。后来老老实实按照题目字面顺序写,才把那个坑给填平了。这也算是给自己的一个警告:在严谨的业务场景里,所谓的“等价优化”往往藏着意想不到的雷。
接下来要补的课还有不少。 我现在的异常处理写得还是像纸糊的,稍微碰到点非数字的字符,程序就直接报红崩溃了。以后得好好研究一下怎么用 try-catch 把这些意外情况温柔地接住。再就是那些设计模式,虽然现在还没正式用,但我知道“策略模式”绝对是治我那个“硬编码力臂”病症的良药,以后得花时间啃一啃。另外,对浮点数精度的处理,以及在边界情况下如何保证数据的完整性,也是我下一步需要专门练习的重点。
最后给课程提点小意见吧。 特别希望以后测试点报错的时候,能多给点具体的反馈,比如直接告诉我“是输出的格式不对”还是“算出来的重心数值有偏差”,这样我就能精准定位,不用像无头苍蝇一样瞎试了。另外,如果作业交完之后能开放看看脱敏的满分解法,我真心觉得比只看一个分数有价值得多,毕竟看着别人的代码,才知道自己的架构差在哪。
最后,感谢这三次作业的“折磨”。从负重前行到精准配平,我学会的不仅是怎么写 Java 代码,更是怎么像一个工程师一样去思考系统的边界与责任。代码会过时,但“分而治之、职责单一”的设计思维,会一直陪着我走下去。
浙公网安备 33010602011771号