309
社区成员
发帖
与我相关
我的任务
分享刚接触 JML 的时候,其实是有些不适应的。以往写代码,拿到需求就习惯性地去构思具体的控制流和数据流,而 JML 强迫我把视线从“怎么做”转移到了“做什么”上。以前写方法往往是凭着对需求的感性理解,但有了 JML 之后,无论是前置条件的免责声明,还是后置条件对状态转移的严格承诺,亦或是对副作用的精准限制,都让我意识到契约式编程本质上是在为代码的行为兜底。自然语言总是充满歧义,但基于离散数学逻辑的规格却能白纸黑字地界定清楚模块间的责任划分,这种开发视角的上移,让我在动手敲代码前就对整个系统的状态转移有了一个清晰的底盘。
这个单元的JML直接改变了我写单元测试的习惯。在以前的作业里,写测试多少有点“黑盒试探”的味道,很多边界情况全凭直觉去凑数据。但引入 JML 后,JUnit 测试就变成了对规格逻辑的严密同构映射。
在具体的测试实践中,我发现对于声明了 pure 的方法,测试的重点不再仅仅是跑出一个正确的返回值,而是必须通过自定义的 strictEquals 或者深拷贝对象,去断言方法执行前后的系统状态绝对没有发生变异。而对于规格中复杂的异常控制流,JUnit 的 assertThrows 机制也成了排雷利器。对照着 JML 里的 exceptional_behavior 逐一构造诸如硬币不足、ID重复等触发条件,让我的代码在提交评测前就有了很高的确定性,不再像以前那样提心吊胆地等强测结果。
这三次作业的迭代过程,其实就是一个不断在“满足规格”和“突破性能瓶颈”之间寻找平衡点的过程。
在容器的选择上,指导书里的 JML 经常会用数组或列表的思想来描述数据集合,甚至会有双重循环的数学表达。如果真在工程里机械地把它们翻译成 ArrayList 并去遍历,在强测的压力下绝对会超时。因此,从第一次作业起,我就有意识地将 Network 和 User 中的核心存储结构全面替换为 HashMap 和 HashSet。特别是到了第二、三次作业,随着硬币体系和视频互动功能的加入,诸如 watchedVideos、likedVideos 等属性激增,这种哈希映射把大量基于 ID 的状态校验压缩到了常数时间,保证了最基础的查询效率。
除此之外,真正的性能瓶颈往往藏在复杂的图算法里。在第二次迭代中,最让我头疼的是求解最长递减序列(queryLongestDecSeq)指令。如果在 10 秒的 CPU 限制下直接拿朴素的深度优先搜索去硬算,指数级的搜索空间一定会导致 TLE。为了解决这个痛点,我在底层架构中引入了记忆化搜索,通过维护一个专门的 Map 来缓存每个节点作为起点的最长序列长度。这种对重叠子问题进行状态剪枝的操作,不仅完全符合 JML 的行为规约,还把时间复杂度降到了可控范围内。到了第三次作业引入智能推荐系统时,为了避免每次查询 UP 主影响力都去全量遍历,我也很自然地采用了在点赞、投币等事务发生时同步维护统计缓存的策略,算是把“空间换时间”的优化思路用熟了。
在这个单元的历练中,我的程序也暴露出了一些由于对规格解读不深而引发的 Bug,主要集中在事务的原子性和容器的别名问题上。
印象最深的一次是在处理像购买粉丝勋章这种涉及多个对象状态转移的操作。一开始我的逻辑是走一步看一步,先把用户的硬币扣了,但在后续检查时如果触发了条件不符的异常抛出,那个已经被扣掉的硬币就成了“脏数据”,这直接违反了规格里对异常情况不可修改状态(assignable \nothing)的强制约束。吃过这次亏后,我把所有的条件前置校验严格放在了方法的绝对顶端,确认万无一失后再集中执行写操作。另外,为了图省事直接返回内部容器引用而掉进浅拷贝陷阱,也让我被查出了数据篡改的 Bug,最后只能老老实实改成返回深拷贝来规避外部的越权操作。
在理解冗长的 JML 和构造 JUnit 时,我也尝试了让大模型来做辅助。不得不承认,在处理规整的逻辑翻译和生成基础测试模板时,大模型确实是个好帮手。但我也发现,它存在严重的战略盲区。比如遇到嵌套的量词约束,大模型往往会毫不犹豫地写出 O(N^2) 的暴力代码,它几乎完全不在乎时间复杂度和整体的容器设计。这让我清醒地认识到,AI 现阶段更像是一个尽职的“语法翻译官”,但真正能对系统性能和架构大局负责的“架构师”,还得是我们自己。
研讨课上的 JML“击鼓传花”游戏中,在这个过程中,我确实发现了大家写 JML 时常犯的隐蔽 Bug,比如写后置条件时漏掉用 \old 去约束原有状态,导致原本“插入新元素”的语义变成了允许其他历史数据被随意篡改。更直观的是,需求边界在几轮传递中发生了严重的畸变——原本严谨的 <= 莫名其妙地变成了 <,生动的业务意图也完全丢失,只剩下冷冰冰的数组操作,口头语言与形式化语言之间的信息损耗展露无遗。
这给了我极大的警醒。经过这次规格训练,我认为今后组队编程必须定死一个规矩:接口与规格先行。在任何人动手写底层控制流之前,整个团队必须先用类似 JML 的严谨契约,白纸黑字地敲定核心接口的入参、返回值和状态转移边界,彻底消灭自然语言里的“大概”、“左右”等模糊字眼。只有把没有任何歧义的契约作为组内唯一的交流准则,并严格据此进行防御性编程,我们才能真正对齐理解,抹平令人头疼的信息差。