OO 第三单元博客总结

王新宇-24231208 2026-06-10 17:19:08

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

1.1 JML 的核心价值

JML(Java Modeling Language)作为一种行为规格描述语言,其核心价值在于将"做什么"与"怎么做"分离。在本次作业中,官方提供了完整的 NetworkInterfaceUserInterfaceVideoInterface 接口及其 JML 规格,我们只需要根据规格实现代码,而不需要猜测方法的预期行为。

JML 的几个关键 clause 的理解:

  • requires(前置条件):定义了方法调用的合法前提。例如 requires rank > 0,意味着调用方必须保证 rank 为正整数,否则方法行为未定义。这实际上是一种契约——调用方满足前置条件,实现方保证后置条件。
  • ensures(后置条件):定义了方法执行后必须满足的状态。它是正确性验证的核心依据,也是编写单元测试断言的直接来源。
  • assignable(可修改范围):明确限定了方法可以修改的变量/容器集合,这是一种帧条件(frame condition),防止实现者产生意外的副作用。
  • **pure**:标注方法为纯查询方法,不产生任何副作用。对于 pure 方法,调用前后的系统状态必须完全一致。
  • signals(异常条件):精确定义了在什么条件下抛出什么异常,这对异常处理的正确性至关重要。

1.2 safe 方法的 side effect 限制

本次作业对 safe 方法做了拓展约束:

  1. 不可在容器或对象中增加 JML 没有要求加入的对象
  2. 不可删除 JML 没有要求删除的对象
  3. 不可修改 JML 描述中涉及之外的对象或属性(object representation 前后一致)

这意味着实现 safe 方法时,每一步操作都必须在 JML 中有依据,不能"顺手"做一些额外的事情。

1.3 规格驱动开发 vs 传统开发

维度传统开发规格驱动开发
需求来源口头/文档描述,容易产生歧义JML 精确描述,无歧义
正确性标准模糊的"能跑就行"ensures 和 signals 的精确满足
测试依据根据理解自行构造直接从 JML 提取断言条件
边界处理容易遗漏signals 明确了所有边界和异常
多人协作容易出现理解不一致规格即契约,统一所有人认知

二、JUnit 测试经验总结

2.1 测试策略

本次作业要求为 recommend_Nth_up 方法编写 JUnit 单元测试,需要对 JML 的全部内容进行检查,包括 requiresensurespureassignable 等。

测试用例设计原则:

  1. 正常路径测试:构造满足所有前置条件的场景,验证 ensures 中的后置条件。
  2. 异常路径测试:逐一触发 requires 不满足的情况,验证正确的异常类型被抛出。
  3. 边界条件测试:如 rank 等于候选 up 主数量的精确值、rank=1(第一个)等。
  4. pure 方法状态一致性测试:调用 pure 方法前后,用 strictEqualsgetUsers() 检查系统状态是否改变。
  5. 同分排序测试:当多个 up 主分数相同时,验证是否返回 ID 最小的。

2.2 关键测试技巧

控制变量法构造同分场景:

  • 分析 computeUpScore 的计算公式,识别所有影响变量(getInterestgetInfluence
  • 确保待比较对象在所有影响变量上取值完全一致
  • 只让待验证的优先级维度(如 ID)不同,其余维度严格相同
  • 避免单边操作(如只观看其中一个 up 主的视频)导致隐含变量失衡

利用辅助方法:

  • strictEquals() 方法可以快速比较两个 User 对象在方法调用前后是否一致
  • getUsers() 方法返回全体用户的浅拷贝,可以遍历检查全局状态

2.3 测试覆盖的 JML clause 对照表

JML Clause测试内容示例
requires前置条件不满足时抛异常userId 不存在 → UserIdNotFoundException
ensures返回值正确性返回的 up 主 ID 应为第 rank 名
signals各种异常分支rank ≤ 0 → InvalidRankException
pure调用前后状态不变调用前后所有 User 对象 strictEquals 为 true
assignable未修改不该修改的变量调用前后 videos 容器不变

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

3.1 迭代概览

作业核心内容新增类/属性新增方法
hw9(第一次)基础社交网络User, Network, VideoaddUser, followUser, watchVideo 等基础操作
hw10(第二次)视频交互系统新增 coins, medals, contributors, commentslikeVideo, coinVideo, forwardVideo, sendComment 等
hw11(第三次)智能推荐系统新增 typeCounts, videos 列表recommendVideo, recommendNthUp, queryMostInfluentialUp, queryUserProfile, queryGlobalBestContributor

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

  1. 对比接口文件:每次作业都会更新官方包中的 Interface 文件,通过对比新旧版本的接口定义,可以发现:

    • 新增的方法签名和 JML 规格
    • 已有方法的 JML 是否发生变化(如 ensures 条件的增减)
    • 新增的异常类
  2. 关注属性变更:本次作业中 User 类新增了 typeCounts(各分区观看数)和 videos(发布的视频列表)。这意味着:

    • watchVideo 方法需要同步更新 typeCounts
    • uploadVideo 方法需要同步更新 videos
    • 这些变化需要从 JML 的 assignable clause 中发现
  3. 注意返回值类型变化:如 getHeat 从浮点改为整数,这直接影响 computeVideoScore 的实现和溢出处理。

3.3 如何发现程序的性能瓶颈

  1. 分析时间复杂度

    • queryMutualFollowingSum:暴力遍历是 O(n²),用户量大时成为瓶颈
    • queryLongestDecSeq:使用记忆化 DFS 优化到 O(V+E)
    • recommendNthUp:需要遍历所有用户并排序,O(n log n)
  2. 容器选择

    • HashMap 用于 O(1) 查找(如 containsUsercontainsVideo
    • ArrayList 用于有序列表(如 contributorscontributions
    • 注意 ArrayList.remove(Object) 是 O(n) 操作
  3. 防溢出处理

    • computeVideoScoregetHeat() * getInterest() 可能溢出,需先转为 long 再相乘
    • computeUpScore 中累加多个乘积,同样需要 long 运算

四、Bug 分析

4.1 Bug 列表与原因分析

Bug 1:queryBestContributor 初始值设置错误

  • 现象:当所有贡献者的贡献值都为正数时结果正确,但在某些边界场景下返回错误的 ID。
  • 原因bestId 初始值设为 -1 而非 Integer.MAX_VALUE。JML 要求同贡献值时返回 ID 最小的贡献者,比较条件 contributorId < bestId 中,如果 bestId 初始为 -1,则永远不会被替换。
  • 修复:将 bestId 初始值改为 Integer.MAX_VALUE
  • 教训:求最小值的算法中,初始"哨兵"值必须足够大。

Bug 2:recommendNthUp 擅自添加异常检查

  • 现象:当用户没有观看记录时,recommendNthUp 抛出了 ColdStartVideoException,但评测系统认为不应抛出此异常。
  • 原因:阅读 JML 的 signals clause 时不够仔细,recommendNthUp 的异常列表只有 UserIdNotFoundExceptionInvalidRankExceptionNoVideoUploadedExceptionColdStartUserException不包含 ColdStartVideoException
  • 修复:删除多余的异常检查代码。
  • 教训:JML 的 signals 是完备的异常列表,不在列表中的异常不应抛出。

Bug 3:coin_video 调用前未预充值

  • 现象:测试时 coinVideo 抛出 InsufficientCoinsException
  • 原因:测试用例中没有先调用 addUserCoins 为用户充值,导致用户余额为 0。
  • 修复:在测试中先执行 addUserCoins 操作。
  • 教训:测试用例的构造需要考虑完整的操作序列。

Bug 4:likeVideo 未检查自己点赞自己的视频

  • 现象:用户可以点赞自己上传的视频,导致测试结果不符。
  • 原因:JML 中 likeVideosignals clause 包含 EqualUserIdException,即当 userId == video.uploaderId 时应抛出异常,但实现时遗漏了这个检查。
  • 修复:添加 if (userId == video.getUploaderId()) throw new EqualUserIdException(userId);
  • 教训:每一个 signals clause 都需要对应一段检查代码。

Bug 5:computeVideoScore 中 int 乘法溢出

  • 现象:当 heat 和 interest 较大时,计算结果出现异常值。
  • 原因:Java 中 int * int 仍为 int,结果溢出后才赋给 long 变量,为时已晚。
  • 修复(long) video.getHeat() * user.getInterest(...) 先将其中一个操作数转为 long。
  • 教训:Java 的类型提升不会自动发生,必须在运算前显式转换。

五、大模型在规格驱动开发中的使用

5.1 大模型的优势

  1. JML 到代码的翻译:大模型可以快速将 JML 规格翻译为 Java 实现代码,尤其是 requires → 异常检查、ensures → 返回值/状态更新 的映射。
  2. 异常处理生成:根据 signals clause,大模型可以系统地生成所有异常分支的检查代码,减少遗漏。
  3. 单元测试生成:大模型可以根据 JML 自动生成覆盖各 clause 的测试用例框架。
  4. 代码模板生成:重复性的 getter/setter/辅助方法可以快速生成。

5.2 大模型的不足

  1. 忽视效率问题:大模型倾向于生成"正确但不够高效"的代码。例如,可能会生成 O(n²) 的遍历来代替更优的算法,或者忽视溢出风险直接使用 int 运算。
  2. 忽视架构/容器选择:大模型可能不会考虑最优的容器选择(如 HashMap vs ArrayList 的权衡),而是按照最简单的方式实现。
  3. 对 JML 的过度解读:有时大模型会添加 JML 中没有要求的额外检查(如 Bug 2 中擅自添加 ColdStartVideoException),这反而会导致与规格不符。
  4. 对 assignable/safe 的理解不深:大模型可能在 pure 方法中不经意地修改状态,违反帧条件。

5.3 使用大模型进行单元测试

基本流程:

  1. 将 JML 规格和实现代码提供给大模型
  2. 要求大模型为每个 signals clause 生成异常测试
  3. 要求大模型为 ensures 生成正确性断言
  4. 要求大模型为 pure/assignable 生成状态一致性检查
  5. 人工审查并修正大模型生成的测试用例,补充边界场景

六、JML"击鼓传花"游戏感悟

6.1 发现的 JML Bug

在击鼓传花游戏中,JML 规格在多人之间传递时容易暴露以下问题:

  • 前置条件遗漏:某个方法没有声明某个边界情况的异常,导致实现者不知道需要处理该情况
  • 后置条件模糊:ensures 中的描述不够精确,不同的人有不同的理解
  • assignable 范围不明确:对"涉及对象"的理解因人而异

6.2 需求与边界的传递变化

在传递过程中,最容易出现的问题:

  1. 语义漂移:A 同学理解的 JML 含义传递给 B 同学时,可能丢失了某些细节
  2. 边界条件丢失:原始需求中的边界条件(如"同分取最小 ID")在口头传递时容易被遗忘
  3. 隐式假设显式化不足:某些实现者认为"显然"的事情,其他人可能并不了解

6.3 多人协作中统一理解的措施

为了在团队编程中减少信息差,可以采取以下措施:

  1. 以代码规格为唯一权威来源

    • 所有需求和边界条件都必须写入 JML 或接口注释
    • 口头讨论的结论必须同步更新到文档/代码中
  2. 建立 Code Review 机制

    • 每个方法的实现者必须对照 JML 逐条检查
    • 另一位同学 review 时重点关注 assignable 和 signals 的完整性
  3. 制定命名和实现规范

    • 统一容器选择策略(如"优先使用 HashMap 做查找")
    • 统一初始值约定(如"求最小 ID 时初始值用 Integer.MAX_VALUE")
    • 统一溢出处理策略(如"涉及乘法的 score 计算统一使用 long")
  4. 编写共享的测试用例库

    • 将边界场景和异常场景的测试用例作为团队共享资源
    • 任何新发现的 bug 都转化为测试用例加入库中
  5. 使用版本控制进行变更通知

    • 接口文件的任何变更都通过 commit message 通知全组
    • 关键变更需在团队群中 @所有人

七、总结

本次单元的学习让我深刻体会到规格化开发的力量

  • JML 将模糊的需求转化为精确的契约,使得"正确"有了可验证的标准
  • 规格驱动开发显著降低了 bug 率,因为每一个边界和异常都有明确定义
  • 迭代开发中,接口文件是最可靠的变更来源,必须养成对比新旧版本接口的习惯
  • 性能优化需要开发者主动思考,JML 只保证正确性,不保证高效性
  • 团队协作中,规格是消除信息差的最佳工具——写下来的才是真的
...全文
26 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

309

社区成员

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

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