一 JML和规格驱动开发的理解
我对“规格驱动开发(Design by Contract)”的理解,可以浓缩为一句话:“规格定义了底线,但仍有很高的优化空间。”
- JML 是一份具有绝对法律效力的“契约”。它用严谨的语言定义了方法执行前需要满足的条件以及执行后必须达到的状态。JML 只关心“What(做什么)”,而完全不在乎“How(怎么做)”。
- 在团队开发中,规格消除了沟通的歧义。调用者只要保证满足 requires,就可以无条件信任被调用者会产出 ensures 的结果,无需去阅读冗长复杂的源码。这极大地降低了代码耦合度,方便了团队合作。
二 JUnit测试的经验
- JML 中最难测试的是 @assignable \nothing(即方法不能有任何副作用)。为了验证这一点,我在测试时总是实例化两个完全一致的 Network(测试组与对照组)。测试组执行被测方法后,利用深拷贝遍历对比两者的每一个状态,任何隐藏的越权修改都会在 Assert.assertTrue 下原形毕露。
- JUnit 不仅要测正常流,更要测异常流。我会根据 JML 中 signals 抛出异常的优先级,刻意构造“同时满足多个异常触发条件”的边缘数据,确保程序抛出的是最高优先级的那个异常。
三 三次作业的迭代过程
本单元的三次作业模拟了一个视频网站社交网络的不断壮大。 - JML 为了数学表达的方便,极其喜欢用一维数组(如 User[])来描述集合。我将所有的底层容器果断替换为了 HashMap<Integer, Object>。到了第三次作业,面对复杂的图路查询,我更是引入了自定义的嵌套结构来替代扁平化存储,这体现了数据结构层面的优化。
- 发现性能瓶颈的诀窍是:警惕 JML 中所有带有 \sum 和嵌套 \forall 的方法。在计算互关对数(queryMutualFollowingSum)或寻找最佳贡献者(queryBestContributor)时,如果每次查询都按 JML 描述去遍历全图,互测时遇到 3000 条连续查询指令,必然导致 TLE(超时)。为了采用读写分离与动态维护。我引入了单独的缓存变量,在网络拓扑发生改变(如 follow, coinVideo)时同步更新这个变量。将昂贵的计算分摊到了写入操作中,使得查询操作的复杂度永远保持在 $O(1)$。
四 出现过的 Bug 及原因分析
- 在处理 watch_video 需要从 ArrayList receivedVideos 中移除视频 ID 时,我曾直接写了 list.remove(videoId)。但这会让 Java 误将 videoId 视作索引(Index)而不是对象(Object),从而导致 IndexOutOfBoundsException。后来通过改为 list.remove(Integer.valueOf(videoId)) 才得以修复。
- 计算用户粉丝年龄段比例(queryUpFollowersAgeRatio)时,如果不将分子乘上 1.0,Java 的整数除法会直接向下取整为 0,导致输出结果全为 0.00。
五 大模型使用心得
- 模型极其擅长“阅读理解”。面对嵌套了四五层的 JML 规格,大模型能在几秒钟内将其翻译成易懂的自然语言伪代码,极大地加速了我对业务逻辑的理解。但一定程度上会忽视效率问题和架构问题,需要明确算法后辅助实现细节。
- 大模型非常适合用来写 JUnit 的 Boilerplate(样板代码)。我会告诉它“请为这个方法生成包括极小值、极大值、全空集在内的 5 个等价类测试用例”,它能迅速帮我搭建好测试脚手架。
六 研讨课 JML“击鼓传花”游戏的感悟
- 我发现,在传递过程中,业务上下文极易丢失。比如原本“硬币转账”的业务,在经历了 JML 抽象再翻译回人话后,变成了干瘪的“修改两个整型变量”。而边界条件在自然语言中更是漏洞百出:“amount 必须为正数”在下一次传递中可能被误写为“amount 不能为负数(混入了 0)”,一字之差,逻辑全非。
- 但游戏证明了,JML 是一种高保真的传输介质。即便前后两位同学完全不知道这是什么业务背景,只要 JML 的数学约束写对了,代码的核心契约就不会发生漂移。
- 未来多人协作时,绝不能仅靠口头约定或简单的 Markdown 文档来分配任务。为了减少信息差,我们必须建立唯一的事实来源,并做好防御性编程。