309
社区成员
发帖
与我相关
我的任务
分享JML本质上是一份"合同"。它用形式化的数学语言描述了方法的前置条件(requires)、后置条件(ensures)、副作用范围(assignable)和异常行为(signals)。与自然语言文档不同,JML消除了歧义——它精确到每一个字段是否可改、每一个异常何时抛出、返回值如何计算。
规格驱动开发的核心思想:先签合同,再写代码。JML只描述做什么,不描述怎么做。
它带来了几个关键体验:
ensures直接告诉你返回值应该满足什么等式,assignable \nothing直接告诉你方法不能改任何东西,测试逻辑从规格中自然推导。getHeat从double改int),对比新旧JML就能精确知道哪些方法的哪些条件变了,不需要重新理解业务逻辑。但同时,JML也有局限。\num_of、\min、\max这些量词在大数据量下无法直接翻译为代码,必须找到等价的算法实现。规格与实现之间的翻译工作,仍然需要开发者自己完成。此外,JML不描述性能要求——两个复杂度差距巨大的实现,可能在语义上同等满足同一份JML。
本单元要求为recommendNthUp方法编写JUnit测试。经过多次重构,我总结了一套三阶段的测试框架:
JML的signals子句不仅指定了异常类型,还通过条件组合暗示了优先级。测试必须按优先级逐层构造场景:
UserIdNotFoundException。InvalidRankException。videos.length == 0触发NoVideoUploadedException。ColdStartUserException。每个测试用例只构造触发目标异常的最简场景,确保低优先级条件不会抢跑。
这是最容易被忽视的测试维度。对于标注assignable \nothing的纯方法,必须在调用前后对所有可观察状态打快照并比较。关键教训:快照必须覆盖全部接口方法暴露的状态,而不仅仅是"看起来会被改"的那几个字段。
在hw11的推荐方法测试中,快照经历了多次迭代才完整:
userCount和coins,漏掉了followers、medals、receivedVideos等。核心经验:快照的覆盖度必须对齐接口暴露的每一个getter。即使是getId()、getName()这类通常不可变的字段,如果被测方法底层替换了对象引用,这些字段也可能被篡改。
对于recommendNthUp,JML用\num_of定义了排名逻辑:排在返回结果前面的更强者人数必须恰好等于rank - 1。测试中需要独立实现计数逻辑(遍历全网、逐个比较得分),而不是依赖被测方法内部的排序算法。这种独立验证的思路是规格测试的精髓。
第一次接触JML,任务是实现用户、视频、关注关系的基础增删改查。最大的挑战是理解JML的数学符号:\forall、\exists、\sum的嵌套组合。典型bug:InvalidAgeException继承了Exception而非MyException,因为没仔细看官方包的继承树——JML的signals只声明异常类型,继承关系需要自己去源码核实。
新增投币、勋章、转发、评论系统。关键变化:getHeat()引入热度计算,purchaseMedal的资金流转改变了多人状态。这一阶段的问题是性能:queryMutualFollowingSum如果每次遍历全图是O(n²),通过维护mutualSum缓存降到了O(1)。同时对拍调试中暴露了countOccurrences的漏洞——JML定义的是重叠计数(\num_of语义),用indexOf循环且步进1(而非keyword.length())是正确的,但O(L²)复杂度在评论数多时会TLE。
第三次迭代新增了6个核心方法:recommendVideo、recommendNthUp、queryMostInfluentialUp、queryUserProfile、queryGlobalBestContributor和computeVideoScore。这次迭代最大的教训来自于对迭代变更的遗漏。
识别迭代中变化的方法:
instance model声明。hw10到hw11,UserInterface新增了typeCounts和videos两个state model。assignable子句。uploadVideo的assignable从videos, users[*].receivedVideos增加了users[*].videos。这个变更如果不仔细看assignable,只会注意到新增方法,忽略已有方法的副作用扩展。getHeat()从double改为int是破坏性变更,编译期就能发现,但前提是你真的去编译了而不是直接copy hw10的代码。识别性能瓶颈的方法:
重查询是本单元的性能杀手。query_most_influential_up每次遍历全用户乘全视频(O(U×V)),recommend_Nth_up排序全候选池并逐个算分(O(U log U × V)),而queryShortestPath和queryLongestDecSeq每次从零开始做图算法。在3000条指令下仅需0.15s,但系统测试的100万条数据直接跑不过。解决方案是在关键热点加缓存——getInfluence结果按用户×类型缓存、最短路径BFS复用、最长链记忆化——但由于公测和互测上限分别为10000和3000条,这些优化对作业提交不是必需的。
Bug 1:getHeat()返回值类型。编译错误,接口要求int但我实现的是double。原因:hw10到hw11的规格变更中只关注了新增方法,忽略了已有getter的签名变化。教训:每次迭代先全局对比接口的所有方法签名,用diff工具而非肉眼。
Bug 2:uploadVideo未更新uploader的videos。getInfluence始终返回0,推荐算法完全失效。原因:JML的assignable users[*].videos子句被忽略,只看了新增方法的ensures,没注意已有方法的assignable扩展。教训:assignable子句是副作用地图,每次迭代必须逐行审查,不能只看ensures。
Bug 3:JUnit测试快照遗漏。case7测试持续失败(纯方法检测未能捕获暗改)。原因:第一版快照只覆盖了"看起来可能变"的字段(coins、followings),忽略了followers、medals、hasReceivedVideo、influences等十几个可观察维度。教训:assignable \nothing测试的快照必须覆盖接口暴露的每一个可观察方法,并以通过版本为参照进行逐字段对齐。
在本单元中,我大量使用了Code Agent辅助开发。以下基于实际使用情况进行总结。
规格翻译:大模型能准确地将\forall、\exists、\num_of等量词翻译为循环和条件判断。对于\min int bestId; (\exists int i; ... (\forall int j; ...))这类嵌套量词,模型比人更能保持逻辑一致性,不容易漏掉量词间的隐含依赖。
测试生成:给定接口的JML,模型可以自动生成覆盖异常优先级、assignable nothing和ensures验证的三阶段测试框架。尤其是快照的全接口覆盖思路——模型在被告知"遍历所有getter"后能系统性地生成检查代码,比人工更不容易遗漏。
变更检测:让模型同时读取新旧版本的接口文件,它能自动列出所有变更点:新增方法、修改的assignable子句、参数类型变化、返回值类型变化。比人工逐行比对高效得多,特别是当接口文件超过500行时。
忽视效率:模型倾向于写出"正确但慢"的代码。例如queryLongestDecSeq,首次给出的实现是每次调用重新DFS全图,没有缓存。在100万条数据下直接超时。需要人工审查后明确要求优化。
忽视容器选择:模型默认使用ArrayList,但followers/following等需要频繁containsKey检查的集合应该用HashMap。hw9的迭代中,O(n)的isFollowing曾导致queryMutualFollowingSum恶化到O(n³),这个教训模型自己不会吸取。
边界理解偏差:\num_of int j; substring(j, j+keyword.length()).equals(keyword)是重叠计数(每个j独立判等),模型有时会忽略这个JML细节,用非重叠的indexOf实现替代。
Agent的核心价值不是"替你写代码",而是"让你更快地发现问题"。在hw11的对拍调试中,Agent在2秒内完成了6组测试数据的编译运行和逐行比对——纯手动作的话,光是diff就要花半小时,而且114万行数据下人为比对几乎不可能不遗漏。Agent不会疲劳,不会走神,这是它相对人工最大的不可替代性。但同时,Agent给出的初始方案往往需要多轮修正——它擅长翻译JML和分析diff,但对性能瓶颈和容器选择这类工程判断,必须由开发者自己把关。
主要存在的问题是:
\forall,\exists,\num_of等。至少对于我出的题目,传递结果是准确的
我认为统一任务需求是有可能实现且有必要的,但统一实现方法是低效且无必要的。JML的核心思想是提供可验证的接口规范,而不是让所有人写出相同的代码。代码本身的实现方法、容器选择、算法优化等,都是实现细节,我们不应当也没必要在团队协作的时候对这些细节进行统一。但是,JML给我们提供了一个团队代码的思路,我们只要求大家的实现符合一个统一的接口,并规定实现方法最终达到的效果,至于方法内部,是类似黑箱的结构,除非出现性能瓶颈,我们本身并不关系具体的实现细节。