2026 BUAA_OO_Unit3总结

胡静伊-24373125 2026-05-29 00:16:38

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

在接触规格驱动开发之前,我写代码往往是“边想边写”,想到哪里补哪里。但这次作业强制要求先阅读JML规格,这彻底改变了我的开发习惯。我对规格驱动开发的理解可以概括为两点:

  • JML就是一份严格的“契约”,分离“做什么”与“怎么做”。比如在 NetworkInterface 中,规格通过 requiresassignableensures 明确规定了方法的前置条件、副作用和后置条件。作为开发者,我只需要保证代码的执行结果满足 ensures 的约定即可,至于内部是使用哪种具体的数据结构,JML并不干涉。这给了我极大的底层实现自由度。

  • 自然语言的需求往往会遗漏边界情况,但JML的逻辑表达式却极其严谨。在实现 addUseruploadVideo 时,规格明确列出了抛出 EqualUserIdExceptionUserIdNotFoundException 等异常的精确条件。这逼迫我在编写核心逻辑前,必须先把防御性代码写好,大大减少了后期的Bug调试时间。


JUnit测试的经验总结

在这次作业中,我花了不少精力编写 NetWorkTest.java。通过编写测试,我总结出了几条行之有效的经验:

  • 测试社交网络最繁琐的就是造数据。为此,我专门写了 build1()build2() 这样的工厂方法,用来快速构建包含多个用户和复杂关注关系的初始网络。这让我的 @Test 方法变得非常干净,能够专注于测试具体的业务动作。

  • 我发现仅仅用 assertEquals 是不够的,对象的状态非常复杂。因此我自定义了 compareUsrcompareVid 方法。在验证用户是否相等时,我不仅仅对比引用,而是通过调用 strictEquals 方法严格校验用户的 ID、年龄和名字是否完全一致。

  • 我的测试用例没有死板地按方法分类,而是按照业务场景来覆盖,例如空图 (testEmpty)、单向/双向关注 (testSingleDir, testOnePair) 以及状态变更后的对比 (testAfterUnfollow)。


三次作业的迭代历程

  • 第一次作业网络还只是个简单的有向图。主要的挑战是图的广度优先搜索(BFS)求最短路径,以及维护互相关注的人数。在这个阶段,我学会了放弃 List 换成 HashMap,并初次尝试了动态维护(全局 sum)。

  • 第二次作业业务场景引入了视频分类、硬币、热度。JML 规格中开始出现大量的嵌套遍历(比如求年龄递减最长序列、求单个 UP 主的最佳贡献者)。纯粹的模拟已经行不通了。我在这时引入了分类索引videosByType)、脏数据标记(Dirty Bit)记忆化搜索memo),让查询从 O(N) 降维打击到 O(1)。

  • 第三次作业全局最佳贡献者、个性化视频推荐(recommendVideo)、UP主推荐(recommendNthUp)等极度消耗性能的需求接踵而至。JML 规格写得轻描淡写,但如果照抄规格,复杂度可以到 $O(N^3)$。在这一代代码中,我再次利用缓存缓存机制,做到了循环内的常数级优化。


如何发现已有方法/容器在迭代中的变化?

  • 当 JML 的 ensures 子句中出现了对特定属性的筛选条件时,就是重构容器的信号。比如HW2 中新增了按 type 查询最热视频的规格。如果只用全局的 HashMap<Integer, Video>,每次都要遍历全网视频判断类型。因此我立刻写出了 HashMap<String, List<Video>> videosByType 这个二维容器,这就是“基于查询维度的索引化”。

如何精准揪出并消灭程序的性能瓶颈?

虽然JML只描述逻辑,但我们可以通过“数理化”它的规格来预判性能瓶颈。总结下来,揪出瓶颈的方法有以下三招:

  • 如果在 JML 中看到一个 \max 里面套着一个 \sum,这在代码里大概率就是一个 O(n^2) 的双重循环,绝对会 TLE。比如说在 HW3 中求全局最佳贡献者(queryGlobalBestContributor),如果现场算,需要遍历所有 UP 主,再遍历他们的粉丝。为了解决这个瓶颈,我在 Network 中引入了 bestCounts 容器。在每一次发生 coinVideo(投币)动作时,顺手计算并更新旧的 bestContributor 和新的 bestContributor 的次数。将原本 O(n^2) 的查询,平摊到了每一次投币的 O(1) 操作中。

  • 识别“读多写少”的图查询,引入“脏标记缓存”,当一个涉及全图遍历的查询(如 HW2 的求最长年龄递减序列 queryLongestDecSeq)可能被连续调用多次,而中间没有任何节点发生变化时,重复搜索是极大的浪费。我设计了 dec 缓存和 memo。只有当 addUserfollowUser 等真正改变图拓扑的写操作发生时,才调用 graphChange() 清空缓存。这种机制让连续查询瞬间返回。

分析自己程序出现过的bug以及Bug出现的原因

本单元作业只出现了两个bug,一个是第一次作业的时候没有注意控制时间复杂度,导致双层循环超时,后期一直注意减少时间复杂度,没有再TLE过。
第二个bug是因为删除评论包含的字符串参数可能为空,并没有注意到这个细节,所以出错了。

Unit3第二次研讨课上,JML“击鼓传花”游戏的感悟: 你是否发现了自己/别人的JML的bug? 在传递过程中,需求,边界是否发生了变化? 今后多人组队编程时,你认为怎么做才能统一所有人对任务需求和实现方法的理解?采用什么措施(或者指定规则)可以减少组内成员间的信息差?

我发现有人在传递过程中漏写了相关变量的类型,比如整型,而我自己也不小心看错了一次数据的边界条件,导致数据范围变化。
我认为还是JML和自然语言双重验证比较合适,并且善用git,让所有人对彼此的改变可见,也可以维护一个共享文档,撰写要求的人可以加粗标出自己认为别人可能遗漏的地方,并且支持大家随时询问和回答。

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

309

社区成员

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

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