2026OO Unit3博客总结

龙毓凡-24371411 2026-05-29 19:57:00

一、对 JML 和规格驱动开发的理解

JML 的核心价值在于消除歧义并提供契约化协作。JML通过前置条件(requires)、后置条件(ensures)、副作用限定(assignable)和不变量(invariant)精确描述方法的行为。在 Unit3 作业中,官方提供了完整的 JML 规格,我们只需按规格实现。自然语言描述的“如果用户不存在则抛出异常”容易出现理解偏差,而 JML 的 signals (UserIdNotFoundException e) !\exists User u; users.contains(u.id); 精确表达了这一语义。同时,规格作为契约,允许多个开发者只关注接口的 pre/post 条件,无需了解内部实现。例如我在实现 recommendNthUp 时,只依赖 User 类提供的 computeUpScore 方法,而不关心其内部如何计算兴趣分,这极大降低了耦合。

规格并非万能,它更适合描述状态转换而非复杂算法逻辑。例如 queryLongestDecSeq 要求返回最长递减子序列的长度,但 JML 难以描述“递减”和“子序列”的图论关系(需要借助 \exists 量词嵌套),官方给出的规格仅是一个框架,具体实现需要自行设计算法。这让我意识到:规格是精确的需求,而非实现蓝图。在团队协作中,先写 JML 再编程,相当于先用形式化语言锁定需求,能避免后期因理解不一致导致的返工。

二、JUnit 测试经验

镜像比较法是我在本单元最核心的测试策略。我构建了一个 mirror 网络,在执行待测方法后,通过 assertAllStateEquals 对比镜像和原网络的所有状态(用户信息、视频信息、互关总数、最热视频等)。这种方法能一箭三雕:检验方法是否产生了未预期的副作用(例如不该修改的数据被修改),验证异常抛出后状态不变,以及测试推荐、排行榜等查询方法是否仅读数据。

边界构造和简单性能测试同样重要。对于 recommendNthUp 这种复杂排序方法,我构造了分数相等时按 id 升序的测试用例:让两个 up 主对用户的兴趣分完全相同,验证推荐返回 id 较小的 up 主。这种显式构造边界的方法比随机测试更有效。性能方面,虽然 JUnit 通常不用于性能测试,但我在本地通过循环调用 queryShortestPath 和 queryLongestDecSeq,观察数据量增长时的耗时,发现 queryLongestDecSeq 的 O(n²) 算法在用户数达到 2000 时明显卡顿,从而指导了后续优化。

三、三次作业迭代过程与性能优化

迭代过程中,规格变化是识别已有方法/容器需要修改的关键。三次作业功能逐次增加:第一次实现基础社交功能(用户注册、上传视频、关注/取关、BFS 最短路径);第二次引入视频互动(点赞、投币、转发、评论、购买勋章)和热度计算;第三次增加推荐算法(推荐 up 主、推荐视频、最长递减序列)。每次作业发布新版本的 JML,我会用 diff 工具对比接口方法的变化,新增方法意味着要在 Network 和 User 中添加相应的数据结构和逻辑。例如第三次作业新增 recommendNthUp,它依赖于 User.computeUpScore,而该方法又依赖于 User.getInterest 和 User.getInfluence。在实现新方法时,我需要确保底层方法(如 getInterest 中的 typeCounts 和 watchedVideos)的正确更新,我重新审视 watchVideo 中是否正确增加了观看计数。容器的变化体现在 Network 中新增了 mostPopularVideos(按类型缓存最热视频),原有 users 和 videos 保持不变,但每次点赞、投币等操作后需要调用 updateMostPopularVideo 或 rebuildMostPopularVideo 来维护缓存一致性。

性能瓶颈集中在图遍历和动态规划上,优化策略需要根据数据特点选择。第一,queryLongestDecSeq 要求基于关注关系的最长递减年龄子序列(严格递减),我最初使用 O(n³) 的 DP 枚举所有路径,后发现可以转化为拓扑序 DP:按年龄排序后,对每个用户遍历其关注的人(年龄必须小于当前用户),状态转移 dp[u] = max(dp[v]+1),复杂度 O(N²)。虽然 N 可达 10⁴ 时仍较慢,但作业数据范围下已足够。第二,recommendNthUp 每次调用都要遍历所有候选 up 主并计算分数,复杂度 O(N²),若频繁调用可考虑预计算兴趣分矩阵,但作业中调用次数有限,未做过度优化。经验总结:不要过早优化,优先保证正确性,在遇到性能问题时再针对热点方法优化。使用 LinkedHashMap 保持插入顺序用于榜单,比 TreeMap 更高效;缓存(如 mostPopularVideos)能显著减少重复计算。

四、程序 Bug 及原因分析

hw_9中我主要的bug是,对于容器我使用的是ArrayList,导致每次查找时都要遍历,导致性能下降;并且我没有对互关人数进行一个缓存,导致每次查询互关总人数时都要重算一边,强测直接TLE了。

hw_11的bug是int类型溢出,在 sortCandidates 方法里,对于usre.getId()的比较我是直接用减法(当时脑子晕了,没考虑到溢出),这种比较方法会导致结果的溢出,改成直接比较就好了。

五、研讨课“击鼓传花”游戏感悟

在传递过程中,我发现了 JML 规格本身的多处 bug 以及需求的畸变。有的同学写的规格我看不懂,有的同学的自然语言需求难以用JML写出,到了最后,大部分的小组最后的需求和原本题目的需求已经有很大差别了。有的连返回值和 exception 都和原本的题目不一样。

针对多人组队编程,我认为应从契约、术语和测试三个方面统一理解。第一,使用 JML 作为契约:所有公共接口必须附 JML 规格,并强制使用工具(如 OpenJML)进行静态检查,确保规格的语法正确性和逻辑一致性。第二,建立共享的术语表:例如明确“互关”是指 id1 关注 id2 且 id2 关注 id1,“冷启动”是指用户没有任何观看记录等,避免理解偏差。第三,实施测试先行和持续集成:基于规格编写 JUnit 测试用例,作为集成验收的标准;测试用例必须覆盖所有异常分支和边界条件;将代码仓库配置 CI,每次提交自动运行全部单元测试,确保无人破坏已有功能。此外,定期的规格评审也很有必要——团队集体审查 JML,重点关注前置条件、异常顺序和副作用范围。通过上述措施,可以将组内信息差降到最低,让协作更高效、代码更可靠。

个人感悟:第三单元让我深刻认识到,没有规格,协作就是灾难。今后参与团队项目,我会率先推动规格文档 + 单元测试 + Code Review 的工程实践,这能减少因理解不一致导致的 bug。同时,JML 训练也提升了我的逻辑表达能力和严谨性,即使不用形式化语言,在写自然语言需求时也会下意识思考边界条件和异常情况。这是本单元最大的收获。

...全文
18 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

308

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧