OO-第三单元博客作业

王渊博-24371201 2026-05-28 11:37:50

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

这三次作业做下来,我对 JML 的理解从一开始的“看懂注释”逐渐变成了“用规格来约束程序行为”。最开始我会更习惯从代码实现出发,先想容器怎么设计、方法怎么写、异常怎么抛;但第三单元真正训练的是另一种思路:先看规格,再写实现。也就是说,代码不是凭直觉写出来的,而是为了满足 JML 中的前置条件、后置条件、副作用范围和异常行为而写出来的。

JML 给我的第一个明显感受是,它把“一个方法应该做什么”说得非常细。例如一个方法不仅要说明正常返回时结果是什么,还要说明什么时候抛什么异常,异常之间有什么优先级,方法是否允许修改对象状态,修改范围是什么。这一点在 recommendNthUp 这类方法中特别明显。它并不是简单地“推荐一个用户”,而是要严格满足候选集合的定义:不能是自己,不能是已经关注的人;排序规则是分数高优先,分数相同 id 小优先;rank 不合法、用户不存在、没有视频、候选数不足时分别抛不同异常,而且异常顺序也不能乱。只看自然语言需求,很容易写出一个“看起来差不多”的方法;但按 JML 写,就必须把每个边界条件都落实。

第二个感受是,规格驱动开发会反过来影响数据结构设计。第一次作业里我用最直接的 ArrayList 遍历完成所有查询,因为这样最容易保证正确,但是TLE了。这时不能脱离 JML 优化,而是要在不改变规格含义的前提下重新组织容器。例如用 HashMap 保证 id 到对象的快速查询,用 TreeSet 维护某类视频热度排名和某分区最有影响力 UP,用缓存维护最长递减年龄序列,用全局贡献者管理器维护 queryGlobalBestContributor 的答案。JML 规定的是行为,而容器设计负责让这种行为在大数据下仍然高效。

我认为真正的规格驱动开发,不是写完代码再看看规格有没有冲突,而是从规格出发确定方法行为、异常优先级、可修改状态、数据结构和测试样例。

二、JUnit 测试的经验

这三次作业中,JUnit 测试给我的最大经验是:不能只测“正常样例”,更要测异常、边界、状态不变性等各种意想不到的细节。

第一类是正常功能测试。直接测试每个函数的功能,只需要涵盖全面即可,这样测试的可信度比手写答案更高。

第二类是状态不变性测试。对于规格中不应该改变状态的方法,我会在调用前后比较整个网络中用户的可观察状态。这种测试能抓到一些隐藏 bug,比如推荐函数中不小心修改了关注关系、观看记录、贡献值、收到的视频列表等。

总的来说,JUnit 的经验是:测试不能只验证能不能跑通junit,而要尽量贴近 JML。要根据JML的每一句话仔细揣测需不需要进行junit测试,防止漏掉很细微的点。

三、三次作业的迭代过程分析

三次作业整体上是一个逐步扩展的过程。第一次作业主要是建立基本框架:用户、网络、关注关系、基本查询和异常统计。最容易超时的函数就是互关函数,如果用Arraylist就是O(n^2)的时间复杂度,会超时(我第一次就超时了),后来修复时添加了一个缓存,在增减成员时维护了一个互关数量,把时间复杂度降维O(1)。

第二次作业在原有用户关系的基础上加入了更多对象和行为,尤其是视频相关内容。程序不再只是一个用户图,而变成了用户与视频共同组成的系统。视频有上传者、类型、观看、点赞、投币、评论等信息,用户也会因为观看、点赞、投币产生兴趣、贡献和影响力等状态。

第三次作业进一步加入了推荐、排行榜、用户画像和全局贡献者查询。这个阶段性能压力明显增加。比如 recommendVideo 需要在大量视频中找最适合用户的视频;recommendNthUp 需要在大量用户中按 computeUpScore 排名;queryMostPopularVideo 要按分区查询最热视频;queryMostInfluentialUp 要按分区查询最有影响力 UP;queryGlobalBestContributor 要统计全局最佳贡献者出现次数。此时如果每次查询都从头遍历全体用户和视频,虽然逻辑简单,但在极限数据下会很容易超时。

第一次作业中,用 HashMap 加 ArrayList 已经足够;第二次加入视频后,需要 videoMap、用户观看记录、上传视频列表、贡献者表等,包含LinkedHashmap、Hashset、用String[] 表示Type等;第三次为了性能,又进一步引入 TreeSet、缓存和管理器类,包含小顶堆等新数据结构。容器的选择需要根据实际功能或操作充分考虑哪种是最快的。如果这个数据容器需要多次查找,那么Hashmap就优于Arraylist,如果天然需要排序,那Treeset就更好。

发现性能瓶颈

主要是静态分析复杂度。判断一个方法依赖用户数 U、视频数 V、边数 E、评论数 C 还是 rank。例如 queryShortestPath 本质是 BFS,复杂度大约是 O(U + E);queryLongestDecSeq 如果每次从头计算,也可能接近 O(U + E);recommendNthUp 如果枚举所有用户并维护堆,就是 O(U log rank);recommendVideo 如果每次扫描所有视频,就是 O(V)。如果某个方法的时间复杂度超过O(n^2),那就需要考虑更换数据容器或者通过缓存来降低时间复杂度了。

四、bug分析

我在第一次作业中queryMutualFollowingSum函数直接用了两重for循环,时间复杂度达到甚至超过了O(n^2),导致TLE,强测错了9个点,bug修复时添加了互关数量缓存,在增减成员时判断是否互关,维护互关数量,把时间复杂度降维O(1)。

五、大模型的使用

强模型在翻译JML上功能已经很强大了,但是如果不刻意强调注意时间复杂度,部分大模型不会去进行优化,会直接选最简单的容器和算法。如果提出要求,会根据功能从数据容器和缓存上实现较好的优化,但是也不会从复杂算法上进行优化(比如不会主动使用kmp等,当然可能也没必要)。同时大模型在缩短代码篇幅(或者抽出一个新类)上比较好用,可以避免超checkstyle的行数限制。

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

我是自然语言描述出题人以及做题人。我出的题里,需要对一个数组求一个平均值μ,然后进行一些别的操作,但是在第一棒传递时,这个μ就被当成了传参,直接没有含义了。其他题在传递过程中,“一个绝对值小于1000的整数”有一棒漏掉了整数,有一棒漏掉了绝对值限制,各种小问题层出不穷。

我的感受是:需求在多人之间传递时,最容易丢失的不是“大方向”,而是边界条件、异常情况、状态变化范围和术语含义。一个人用自然语言描述时觉得已经说清楚了,但另一个人理解时可能自动补充了自己的假设;再经过几轮翻译,这些假设就会逐渐偏离原意。因此,在今后多人组队编程时,统一理解不能只靠口头讨论,而应该依赖一套明确的“规格—设计—实现—测试”流程。比如组内需要建立统一术语表,接口先行,要有共同的测试标准,重要决策要记录,建立代码评审规则等。

具体规则可以采用以下几条:所有新增功能必须先写规格再写代码;所有接口修改必须通知全组并更新文档;所有异常优先级必须明确写出;所有查询方法必须说明是否允许改变状态;所有核心方法至少配一个正常样例和一个异常样例;合并代码前必须通过统一测试;任何口头讨论得出的结论都要转化成文字记录。

七、总结

三次作业最大的收获是,我开始把“正确性”和“性能”同时放进规格驱动开发中考虑。JML 告诉我程序应该表现出什么行为,JUnit 和本地评测机帮助我确认行为是否正确,复杂度分析和压力数据帮助我确认程序能不能在大规模输入下运行。

如果只追求功能通过,很容易写出大量临时判断和全局遍历;如果只追求性能,又可能破坏 JML 中对异常、副作用和返回值的要求。真正稳妥的做法是:先按规格明确行为,再用测试守住正确性,最后根据瓶颈有针对性地调整容器和算法。三次作业的迭代过程也说明,面向对象设计不是一开始就能完全定型的,而是在规格扩展、测试反馈和性能压力中逐步重构出来的。

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

305

社区成员

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

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