面向对象第三单元博客

吕政飞-24231211 2026-05-28 17:08:50

面向对象第三单元博客

JML和规格驱动开发

JML(Java建模语言)是一种工具,而规格驱动开发是一种以该工具为代表的开发范式。它们的核心思想是:将代码的“设计意图”(规格)和“具体实现”显式地分离开,并以前者为指导进行开发和验证。

我i们可以把 JML 当作“对外可见行为”的合同:前置条件(requires)规定调用方责任;后置条件(ensures)规定实现方交付;assignable 划定可修改的状态边界;pure代表方法无副作用。而“模型字段/不变式”更像数据结构的抽象视图:接口里写的是 users[]/videos[] 、 following[] 这些序列语义,实现里用 Map/List/Set 只是手段,关键是把这些容器映射回规格的抽象序列,并持续满足 invariant。

SDD 的核心流程是“先定可验证的外部行为,再选实现”:先从异常触发条件、状态变化最小集、输出副作用出发规划代码路径;再考虑如何在不破坏JML约束的情况下,通过缓存、索引等降低时间复杂度。

JUnit 测试经验总结

测试设计更偏“规格覆盖”而不是“代码覆盖”:按每个方法的 normal/exceptional behavior 列出分支,用用例一一击中(例如 hw11 的 assertThrows 覆盖异常优先级)。

此外对于方法的副作用检查也很重要,通过“调用前快照 + 调用后对比”来检查方法不会偷偷改不被允许改的状态。

而为了符合工程化的设计,我们可以把快照/对比写成工具函数(VideoSnapshot/UserSnapshot),减少重复;对复杂结构用“抽取可比较的最小视图”(id、计数、集合成员关系)而不是直接比较对象引用;对字符串/数组用专门断言( assertArrayEquals)。

三次迭代过程与Bug分析

主要是遵循main中接口进行迭代开发,自己的迭代主要集中在对于容器的选取以及对于一些额外状态的维护以提高性能。

在 hw9 中,一开始对于 JML 的了解不够深入,误以为 safe 方法不能改变任何状态(后得知 safe 只是不能改变 JML 中规定了的属性),导致对于互关数量的查询是每次动态计算,且是按照点遍历而不是边遍历,时间复杂度为 O(n²),导致了超时。因此后面对于互关数量的查询改为维护一个互关数量属性,使得时间复杂度降到O(1)。

在 hw10 里,容易出错的点主要在于 queryLongestDecSeq 方法,对于这个方法的视线最好采用 dp 算法,以矩阵的样式实现;如果采用递归方法实现,容易爆栈(虽然结合JML要求好像最多也就递归110层)。

在 hw11 中,主要有以下几个可以提高性能的地方:

  • 对于用户画像的获取,正常遍历WatchedVideo的时间复杂度是O(n),如果维护一个用户画像private final int[] typeCounts = new int[TYPES.length];,则可以做到O(1)。
  • 对于全网最佳贡献者的计算,正常是遍历每一个用户,再遍历这个用户的所有关注者,复杂度是O(n+m)(m是全图有向边的数量),如果每一个用户维护一个最佳关注者,复杂度可以做到O(n)。
  • 对于recommendNthUp的实现,正常实现先获取候选up队列,再进行排序(sort比较器),时间复杂度为O(n + m log m * k),n为用户数,m为候选up数,k为每个up平均视频数;第一层优化:不再在 sort 的比较器里反复调用 computeUpScore(),而是在排序前, 先给每个候选 up 算一次分数并存起来,然后按“分数降序、id 升序”排序,时间复杂度为O(n + m * k + m log m);第二层优化:up.getInfluence(type)会遍历这个 up 发布的所有视频,给每个用户维护一个 各分区影响力缓存(也优化了queryMostInfluentialUp),复杂度为O(n+m log m);第三层优化:因为方法要求第rank个推荐up,对于候选人无需全排序,而是维护一个大小为 rank 或 m-rank 的堆(根据rank的大小进行选择),时间复杂度来到O(n + m log rank)。

大模型的优势与风险

优势:

  • 能把 JML 拆成可执行清单:每个 requires/ensures/assignable → 对应代码分支、对应测试点(尤其异常优先级、\old 的状态迁移)。
  • 能快速枚举边界用例:空集合、极值 id/age、重复操作、冷启动条件、tie-break(id 最小优先)等。
  • 能辅助“从规格反推数据结构”:例如需要稳定顺序就倾向 LinkedHash*。

风险:

  • 大模型容易在把功能跑通”阶段容易生成全表扫描、重复计算、或把序列语义实现成不保序容器,没有进行相应的性能优化。

JML“击鼓传花”感悟

确实难免会有bug,在传递过程中,少写或者漏写前置条件和边界。在自然语言描述中,大家一般会省略一些默认的情况,比如对于作业中的投币功能,用自然语言描述可能就会省略掉硬币数不足的情况。而在 JML 转自然语言的时候,我们组的同学一般都是将 JML 翻译了一遍,虽然不是很符合自然语言的“自然性”,但也确实减少了传递过程中的错误与二义性。

统一对需求的理解:

  • 指定单一真源(Single Source of Truth):需求以规格/接口契约为准(例如 JML)。
  • 把需求翻译成可检查清单:每个功能点拆成“输入/输出/异常/边界/副作用/性能约束”,并明确优先级与异常优先级。

统一对实现方法的理解:

  • 先对齐模块边界与数据流:每个模块只暴露最小公共接口(方法签名、抛出异常、返回语义、复杂度目标),实现细节允许不同人自由发挥但不能突破契约。
  • 固化关键设计决策:遇到分歧点(容器选型、缓存策略、排序规则、并发策略)就用短决策记录(ADR 思路)写清“为什么这样做、替代方案、约束与失效条件”。
  • 规定不可变规则:命名、异常处理顺序、tie-break(如 id 最小优先)、是否允许副作用、容器是否要求有序(HashMap vs LinkedHashMap)等。
...全文
13 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

307

社区成员

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

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