面向对象第三单元总结:从 JML 到规格驱动开发

杨璞-24371240 2026-05-28 18:08:22

第三单元的三次作业围绕一个在线视频平台逐步展开:hw9 维护用户、关注关系和视频接收;hw10 加入硬币、点赞、转发、评论和粉丝勋章等互动逻辑;hw11 继续引入推荐系统和用户画像。这一单元最明显的变化是:题目要求先读懂一份已经写好的规格,再把规格转化成可靠的实现。

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

一开始读 JML 时,我更容易把它看成一种“更严格的注释”。做完三次作业之后,我对它的理解变成了:JML 是接口和实现之间的契约,它规定了方法在什么前提下可以被调用、调用后必须满足什么状态、哪些对象可以被修改、哪些内容必须保持不变。

在这一单元里,requiresensures 决定了基本功能正确性,但真正容易漏掉的是 pureassignable 和各种边界条件。比如一个查询方法不只是“返回值正确”就结束了,它还不能改变容器内容;一个清理评论的方法不只是“删掉含关键词的评论”,还要保证非目标视频不被影响、剩余评论的 id 和内容关系不被破坏、被删除评论对应的索引结构同步更新。

我觉得规格驱动开发带来的最大好处是把“需求理解”前置了。以前写代码时可能先搭框架,再边测边补;而 JML 迫使在实现前回答几个问题:

  1. 这个方法真正允许修改哪些状态?
  2. 异常分支和正常分支的优先级是什么?
  3. 容器里元素的顺序、重复性、唯一性是否有要求?
  4. 查询方法是否可以为了缓存或优化而改变对象?
  5. 某个结果是实时遍历得到,还是应该随增删操作增量维护?

这些问题如果在写代码前没有想清楚,后面就会变成 bug 或性能问题。

二、JUnit 测试经验

这一单元每次作业都要求为一个指定方法编写 JUnit 测试:hw9queryMutualFollowingSumhw10cleanSpamCommentshw11recommendNthUp。我的体会是,JUnit 测试不能只测“几个正常样例”,而要把 JML 翻译成测试点。

1. 测返回值

最直接的是构造小规模网络,手算答案。例如互关数量可以用三角形、单向环、互关后取消关注等数据覆盖;推荐 up 可以构造几个 up 主的视频热度和用户兴趣,让排名结果能够手算出来。

2. 测异常分支

异常不仅要测“会不会抛出”,还要测抛出的优先级是否符合规格。例如 recommendNthUp 中,我专门测试了 rank <= 0、没有视频、候选 up 不足等情况,避免实现里因为检查顺序不同导致错误异常。

3. 测 pure 和 assignable

这是我后面最重视的一点。比如 recommendNthUp 是查询式方法,调用前后用户的关注关系、粉丝关系、看过的视频、点赞状态、硬币数、视频热度、评论状态都不应该改变。因此我在测试里写了快照类,把调用前的关键状态保存下来,调用后逐项比较。

这种测试方法比单纯 assert 返回值更有价值,因为很多“错误代码”会返回正确答案,但为了求答案排序、删除、移动了原容器。规格测试恰恰要抓住这种副作用。

4. 针对边界造数据

cleanSpamComments 的测试让我印象比较深。它的边界并不难,但很细:关键词重叠出现如何计数、连续命中的评论是否全部删除、空评论列表如何处理、删除后 containsComment 是否更新、清理不存在视频时原状态是否保持。这些测试不是随机测出来的,而是从 JML 中逐句拆出来的。

我的经验是:写 JUnit 时先不要急着写很多复杂数据,而是先把规格中的量词、条件分支和“不变状态”列成清单,再每个清单项构造一个尽量小的样例。

三、三次作业的迭代过程

1. hw9:从直接实现到增量维护

hw9 的核心是用户网络和视频传播。我的初始设计是用 HashMap<Integer, User>HashMap<Integer, Video> 做按 id 查询,同时保留 ArrayList 维护用户和视频的遍历顺序。用户内部同时维护 following/followers 列表和对应的 HashMap,这样既能保留顺序,又能把 isFollowingcontainsFollower 这类判断从线性扫描优化到接近 O(1)。

hw9 强测后我修复了部分情况用时过长的问题。原先 queryMutualFollowingSum 每次调用都遍历所有用户对,复杂度接近 O(n^2)。当查询频繁时,这个方法会明显拖慢程序。修改后我在 Network 中维护 mutualFollowingSum,在 followUser 成功建立一条关注边时,如果反向边已经存在就加一;在 unfollowUser 删除一条边前,如果反向边存在就减一。这样查询本身变成 O(1)。

同一次修改还优化了视频推送和最短路查询:上传视频时不再遍历全体用户判断谁是粉丝,而是直接遍历上传者的粉丝列表;BFS 不再维护额外的距离表,而是按层计数。这些优化说明,JML 能告诉我“结果应该是什么”,但不能保证“怎么写都能过时间限制”。规格正确只是第一步,容器和算法仍然要自己负责。

2. hw10:互动系统和状态一致性

hw10hw9 基础上加入了硬币、点赞、投币、转发、评论、勋章和最长下降序列等功能。这次迭代的主要难点不是某一个算法,而是状态数量明显增加:

  • 用户新增了 coinswatchedVideoIdslikedVideoIdsmedals、贡献者列表等状态;
  • 视频新增了 playCountlikesforwardCountcoins、评论 id 和评论内容;
  • 一次操作可能同时修改用户和视频,例如投币既修改用户硬币,也修改 up 主收入、贡献记录和视频硬币数。

明显感觉到:每个容器都要有清楚的“主数据”和“辅助索引”。例如评论使用 commentIds/commentContents 保存顺序和内容,用 commentIndexMap 快速判断 id 是否存在。清理评论时,如果只删数组而忘记重建索引,就会出现 containsComment 与实际评论列表不一致的问题。

hw10 的 JUnit 我更改了很多次才通过中测。 cleanSpamComments中不断补充测试点,从“能删掉含关键词评论”扩展到“只修改目标视频”“删除后索引同步”“重复清理只统计当前存在评论”“视频统计信息不被评论清理影响”。

3. hw11:推荐系统与算法抽离

hw11 新增推荐视频、推荐第 N 个 up、最有影响力 up、用户画像和全局最佳贡献者等功能。相比前两次,这次的新增需求更偏查询和排序,性能压力也更集中。

我在用户中增加了 typeCounts 记录用户观看各分区视频的次数,增加 uploadedVideos 记录用户上传的视频。这样 getInterestgetProfilegetInfluence 都可以直接基于已有状态计算,而不需要每次在全局视频列表里反复扫描。这个变化体现了一个迭代中的容器判断原则:当某个查询反复需要“按用户”或“按类型”聚合数据时,就要考虑在状态变化发生时同步维护辅助容器。

这次我还把 BFS、最长下降序列、全局贡献者统计、推荐 up 的候选筛选和选择逻辑抽到了 NetworkAlgorithms 中。这样 Network 更像业务流程层,NetworkAlgorithms 专门处理算法细节。这个拆分不是为了形式上的“多一个类”,而是因为推荐系统加入后,Network 已经同时承担异常检查、状态更新、输出、查询和算法,继续堆在一起会降低可读性。

我还对 recommendNthUp 做了性能优化:原来先对所有候选 up 全排序,再取第 rank 个;修改后用类似快速选择的方式,只把第 N 个候选放到目标位置。全排序复杂度是 O(n log n),选择算法平均复杂度接近 O(n)。在只需要第 N 名、不需要完整排名时,全排序就是多做了工作。

四、如何发现已有方法和容器在迭代中的变化

三次作业让我形成了一个比较固定的检查流程。

第一步是对照新旧 JML 和指导书,看接口签名、异常、assignable 范围有没有变化。例如 uploadVideohw10 中增加了 typehw11 中虽然签名延续,但用户需要额外维护上传视频列表和类型观看次数。

第二步是从新增查询倒推容器。例如 queryMostInfluentialUp(type) 需要快速知道每个 up 在某个分区的影响力,那么用户对象就应该知道自己上传过哪些视频;queryUserProfile 需要用户各分区兴趣,那么用户观看视频时就应该同步更新 typeCounts

第三步是检查旧方法是否需要维护新状态。比如 watchVideohw9 只需要把视频从“收到但未看”里移除;到 hw10 要加入观看历史并增加播放量;到 hw11 又要更新分区观看次数。很多迭代 bug 都不是新方法写错,而是旧方法忘记维护新增状态。

第四步是检查辅助索引和主容器的一致性。只要一个对象里同时有 ArrayListHashMap/HashSet,每个增删操作都必须成对更新。评论清理、关注/取关、收到/观看视频都属于这种情况。

五、如何发现程序性能瓶颈

我的方法主要有三类。

第一类是看复杂度和调用频率。queryMutualFollowingSum 如果每次都扫所有用户对,在 10000 条指令下很容易成为瓶颈;所以我把它改成增量维护。类似地,isFollowing 如果每次线性扫描关注列表,也会在图查询和推荐中反复放大。

第二类是看是否重复做了可以提前维护的聚合。粉丝年龄比例、用户分区兴趣、up 主影响力、贡献者统计等都可以通过辅助数组或容器降低查询成本。

第三类是看“目标结果是否真的需要完整过程”。recommendNthUp 只需要第 N 个 up,不需要完整排序,因此全排序不是必要的。这个思路也提醒我:性能优化不只是换容器,有时是重新理解问题本身。

六、程序中出现过或暴露过的 bug 与原因分析

1. 性能型 bug:查询时重复遍历

hw9 的超时是最典型的例子。最初我按照 JML 的描述直接计算互关数量,逻辑上正确,但在大规模查询下会超时。原因是我把规格中的数学定义直接翻译成了实现,没有进一步考虑查询频率和复杂度。修复方式是把互关数量变成随关注关系变化同步维护的状态。

2. 容器一致性 bug:主容器和索引不同步

评论系统很容易出现这种问题。删除评论后,如果 commentIds/commentContents 变了,而 commentIndexMap 没有同步清理或重建,就会导致已经删除的评论仍然被认为存在,或者新评论 id 被误判为重复。这个问题的根源是一个抽象状态被多个物理容器共同表示,因此修改时必须维护 representation invariant。

3. 边界理解 bug:关键词计数与删除范围

cleanSpamComments 中,关键词可能重叠出现,例如 "aaaa""aa" 可以出现 3 次。如果只用非重叠计数,就会得到错误的最大出现次数。此外,清理评论只能影响目标视频,不能影响其他视频,也不能改变播放量、点赞数、投币数等非评论状态。这个问题的根源是对 ensuresassignable 的阅读不够细。

七、大模型与 Code Agent 使用经验

第三单元我使用过 Code Agent 辅助阅读与编写代码、整理测试思路和检查复杂状态。我的体会是,大模型在规格驱动开发里最有价值的地方不是“替我写完代码”,而是帮助我把 JML 拆成检查清单。

它比较擅长的事情包括:

  • 根据 JML 列出正常分支、异常分支和副作用边界;
  • 提醒某个方法是否应该是 pure,以及调用前后需要保存哪些状态;
  • 帮助构造 JUnit 的边界样例;

但大模型也有明显风险。它容易根据规格写出直观但低效的实现,例如把 ensures 中的量词直接翻译成多重循环;也可能忽略架构和容器问题,把所有逻辑堆进一个类里。我的使用方式是:先自己确定数据结构和复杂度,再让它帮忙检查遗漏;对它给出的代码,重点审查异常优先级、容器一致性和时间复杂度。

在单元测试方面,大模型比较适合生成测试点列表和小规模样例,但最终断言什么、哪些状态必须保持不变,还是要确认。

八、JML“击鼓传花”游戏的感悟

第二次研讨课的 JML“击鼓传花”让我印象很深。它实际上暴露的是团队协作中常见的问题:每个人以为自己理解了需求,但理解的边界并不完全一样。

我在这个过程中最大的感受是,JML 的 bug 不一定是语法写错,更多时候是“没有把隐含前提写出来”。例如某个容器是否允许重复、异常检查的先后顺序、查询方法是否允许缓存、空集合时返回什么、边界值是否包含等,如果没有在规格中明确,后面的人就可能按照自己的直觉补全。

在传递过程中,需求和边界很容易发生变化。有的人会为了实现方便把条件写得更强,有的人会为了覆盖更多情况把条件写得更弱;有的人关注返回值,有的人关注副作用;还有的人会默认“正常输入”不会出现某些边界。这些变化单独看都不大,但传几轮之后,最初的需求就可能被悄悄改写。

如果以后多人组队编程,我认为减少信息差至少要做到几件事:

  1. 先统一术语。比如“关注”“粉丝”“互关”“收到但未看”“看过”都要对应到明确字段或抽象集合。
  2. 对每个接口写清正常行为、异常优先级、副作用范围和边界返回值。
  3. 维护一份共享测试清单,把每条规格转成至少一个测试点。
  4. 重要修改必须同步更新规格、实现和测试,不能只改代码。

我觉得 JML 在团队合作中最大的价值,就是把“我以为你知道”的部分变成可讨论、可检查、可测试的文字。它不能消除所有沟通成本,但可以让沟通有一个共同对象。

九、总结

第三单元让我真正体会到,规格驱动开发并不是削弱设计,而是把设计的起点从“自由发挥”移动到了“精确理解契约”。JML 负责定义正确性的边界,JUnit 负责把这些边界变成可执行的检查,而容器选择和算法优化负责让程序在规模上站得住。

三次作业中,我的代码从直接实现 JML,逐步变成维护辅助状态、拆分算法类、用快照测试 pure/assignable、用复杂度分析定位瓶颈。这个过程也让我意识到:写对一个方法并不难,难的是在需求迭代后,仍然让所有旧状态、新状态、异常分支、查询语义和性能约束保持一致。

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

309

社区成员

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

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