JML规格化设计
前言
本单元作业是一个助教辛苦,学生幸福的单元,首先感谢助教对于JML的接口规格化设计。
第三单元测试过程
对测试的理解
- 单元测试,目标:验证最小功能单元(函数、类)是否按预期运行。
- 功能测试,目标:验证功能是否符合需求规格。
- 集成测试,目标:验证多个模块组合后的协同工作是否正常。
- 压力测试,目标:测试系统在高负载、大数据量下是否稳定、性能合理。
- 回归测试,目标:确保旧功能在新版本中仍然正确。
数据构造策略
状态跟踪使用 existing_person_ids、existing_relations 等集合保存已创建实体,确保生成的命令多数为有效命令;避免了生成无效命令浪费测试机会。
概率控制 每次生成 ID 时有 80% 以上概率使用已存在的 ID,20% 左右概率生成不存在的 ID,用于测试非法输入处理; 保证测试“有效性 + 鲁棒性”兼备。
模块覆盖与比例平衡明确控制三大模块(HW1、HW2、HW3)的命令分布权重;比如HW3权重更高(如 am、aem),适应后期重点测试。
边界情况生成比如 ID 范围为 [-100, 100],年龄范围为 [1, 200]; 使用不同数据长度(如随机生成长度 1~100 的名字)。
指令生成路径,按照一定的策略随机的生成指定数量的指令,负责测试程序的不同功能。
大模型辅助
一、整体策略:从“提问者”到“指挥者”
大模型的强项是语言理解与上下文记忆,而不是通灵。要发挥其最大效能,关键在于你如何设计任务结构、输入信息与交互节奏。可遵循以下三步法:
目标清晰(What)明确告诉模型你要做什么,如:“我正在开发一个评测机,请帮我生成合理的测试用例生成器。”
约束充分(How)提供必要背景与规则:输入格式、限制条件、边界值、接口定义、已有类说明等。
拆解合理(Step by step)将复杂任务拆解为子任务,让模型逐步完成(如先写接口草图,再实现核心函数,最后补充状态维护)。
二、不同典型场景下的引导技巧
场景一:规格化设计(接口与架构)
目标:设计合理的类、方法、数据结构
引导技巧:
明确提出功能要求(如“实现一个支持动态添加好友、标签的社交网络类”)
说明输入输出格式(如命令格式 ap id name age)
提供已有组件(如你已有的 Person 类定义)
可用 prompt 模板示例:
1 | |
场景二:代码实现(核心算法、状态维护)
目标:写出能工作的函数/类
引导技巧:
提供函数签名与预期行为(如“输入为命令列表,输出为处理结果”)
提供已有代码片段、类定义供调用
可示例性描述一些关键输入输出
若模型写的第一版不够精确,可反复对细节提问并要求“逐行解释你的代码”
场景三:测试与验证
目标:构造测试用例,验证正确性
引导技巧:
给出你期望测试的函数或命令格式(如 “qv id1 id2”)
明确边界(如“我要测试最大长度字符串输入”)
命令示例 + 状态约束(如“已存在的 ID 集合是 {1, 2, 3},请生成合法但边界的 mr 命令”)
场景四:优化与分析
目标:让模型分析瓶颈、重构代码或提出设计优化建议
引导技巧:
提供当前实现代码 + 描述性能瓶颈
提出明确优化目标(如“将当前 getValueSum() 从 O(n) 降为 O(1)”)
要求模型分析时提供理由,不仅是结论
关键数据结构和算法设计
hw9
Person:
1 | |
为了方便查询acquaintance,使用了复杂度为O(1)的HashMap来存储Person的好友。
本次作业中,Person需要注重考虑优化getBestAcquaintance()方法,于是我采用了java自己的treeSet来实现O(logn)维护,O(1)查询,只需要重写Set中元素的compareTo()方法即可线上维护value的顺序。
Tag:
1 | |
本次作业Tag需要注意的点是getAgeMean()和getAgeVar(),本人同样实现了O(1)维护,O(1)查询的方法,只需要维护好value和以及value^2和即可。
NetWork:
1 | |
NetWork的难点主要体现在查询三元环数量以及连通性查询
关于三元环的查询,我选择维护一个triple变量,在每一次加边和删边操作时进行O(N)的维护,实测效果很好。
关于连通性查询,我见许多同学使用了并查集这个数据结构,但是本次作业中涉及到了删边操作,但是并查集并不支持删边后的查询,于是只能重建,鉴于实现起来过于复杂,我选择了BFS查询连通性,这一算法在后续接口中也会使用,只能说性价比极高。
hw10
新增类OfficialAccount:
1 | |
这是本次作业新增的类OfficialAccount,主要负责维护公众号的关注者和文章(仅关注ID)以及相应关注者的贡献。
需要注意的地方只有一个接口getBestContributor(),所以我们选择和上次作业一样的做法,用TreeSet来维护贡献的有序性,然后就可以很快的查询最佳贡献者了。
Person改动:
1 | |
我们需要维护Person收到的文章以及他们接受的顺序,由于在这一次作业中不可能出现一个人多次接受同一篇文章,所以我们用Map来存储article。
但是问题来了,我们怎么维护文章的接受顺序呢?没错,又是TreeMap,我们在Person里维护一个取号机制,来一篇文章取一个号,同时号码减一,然后利用TreeMap能维护Key的顺序的机制,我们就能维护好文章的顺序啦,先进来的文章取得号一定大于后进来的文章。
我们还注意到我维护了一个otherTags,这其实是为了Network里的一个接口服务的,这里暂不讲解。
Tag改动:
Tag新增了一个对于内部Person之间value和的查询,但是这个实现主要是在Network中维护的,所以不在这里讲解了。
Network改动:
新增了双元环数量的查询(当一对Perosn互为最好的朋友时就视为一个双元环),这个维护方法和三元环类似,主要是在网络中加边删边时正确更改即可。
新增了对于TagValueSum的查询,注意到我在Person里维护了Person被归类的Tag,这样就可以在更改关系时查询会涉及到哪些Tag的valueSum,然后一一线上维护即可。
新增了最短路径的接口,我们采用了BFS的实现,简单高效。
hw11
本次作业的改动主要为新增了Message类,Message承担了转发文章(被转发的人会接受),发红包(更改Person的Money值),发表情包的作用,同时会增加转发于被转发者的社交值。
在时间复杂度上没有什么难点,只需要注意处理机制就行,因为这次作业的JML超长!!!!
从规格到实现,性能如何保障?
JML设计者在写规格时想的一定不是具体的实现方法,而是怎么去简单又完美的约束好一个接口的功能与副作用,是站在整体的角度思考问题的,想好了一切可能的行为,分别做好不同的约束处理。
实现则是在遵守JML约束的前提下,尽可能的提高实现的性能。此时工作人员想的是一个具体的实现方法。
JUNIT测试
一、JUnit 测试的本质与作用
JUnit 是 Java 中最常用的单元测试框架,主要用于:
自动化验证类和方法的行为是否符合预期规格;
支持回归测试,及时发现代码修改带来的错误;
增强测试覆盖率(代码分支、边界、异常情况);
与规格(如 JML)结合时,充当规格实现一致性的验证器。
二、利用“规格信息”设计 JUnit 测试的思路
- 明确每个方法的规格(前置条件、后置条件、不变量)设计测试用例时:依据每条规格构造一个正例 + 至少一个反例。
- 将规格翻译为断言(assert)JUnit 提供 assertEquals, assertTrue, assertThrows 等断言方法。
- 结合边界值与等价类分析基于规格设计测试数据,覆盖各类输入场景。
三、JUnit 测试验证规格一致性的效果体现
- 捕捉与规格不一致的实现错误如:
规格要求“相同关系不可重复添加”,但实现却允许重复;
JML 声明“添加后 size 增加”,但实际调用无变化。
通过 JUnit 断言对比前后状态,可快速识别这些逻辑偏差。
提高可维护性与回归保障一旦方法规格变更,只需更新对应测试:避免因手动测试遗漏导致旧功能失效;结合 CI/CD,可以自动在提交时运行测试,保障规格长期一致性。
支持规格驱动开发(Test-first / TDD)JUnit 测试与规格结合,可倒逼实现更加规范:先写规格 → 写测试用例 → 最后写实现,类似于“契约驱动开发”,提升代码清晰度与可预测性。
四、实际项目中的 JUnit 设计示例
假设你有如下 JML 规格定义:
1 | |
可设计如下 JUnit 测试:
1 | |
学习体会
本单元的学习让我真正体会到“规格先行”的重要性。JML 规格不仅仅是对接口行为的约束,更是一种程序设计的思维方式。它迫使我在写代码前先理清楚每一个函数的职责、边界与副作用,从而避免“边写边试、边改边错”的低效流程。
在实现过程中,面对复杂接口和状态维护时,我深刻认识到:良好的设计是性能和正确性的保障。比如在 Person 类中使用 TreeSet 优化最优好友查询、在 Tag 中用数学方法实现均值与方差的 O(1) 维护、以及在 Network 中用 BFS 替代复杂的连通性维护方案——这些都是在规格指引下不断优化的结果。
此外,借助大模型的协作能力,我从“提问者”成长为“任务设计者”。我学会了如何清晰地表达意图、分步拆解任务、构建提示模板,并通过反复调试与细节提问来获得更贴合需求的输出。这种交互方式不仅提升了效率,也让我意识到人工智能更像是一个“逻辑助理”,它不会替代你的判断,而是在你的设计之下发挥最大价值。
最后,JML + JUnit 的组合是一次理论与实践的双重磨砺。它让我意识到代码的正确性从来不是“写完跑一下就行”,而是一种自我约束、一种契约精神。测试不是验收,而是开发的一部分,它推动我不断思考:我的代码,是否真正做到了“言出法随”?
这个单元虽然挑战很多,但正因为如此,我收获的也更多。感谢助教提供的精细规格,感谢这次设计-实现-测试一体化的宝贵锻炼机会。