305
社区成员
发帖
与我相关
我的任务
分享
JML(Java Modeling Language)是一种行为接口规格语言,用于精确地描述 Java 程序模块(类、接口、方法)的抽象行为,而不关注具体实现。
JML使用基于一阶谓词逻辑的注解表达:
规格驱动开发是一种以规格为中心的开发方法,强调先写规格,后写代码,将 JML 规格作为契约贯穿整个开发周期。
自然语言文档可能歧义,JML弥补了这一点,让意图像数学一样精确。通过规格检查,能在编码前发现需求矛盾,只要共同遵守规格,就可以独立开发、替换。当然,动态行为、复杂业务逻辑等写出完整规格可能非常困难,或者用纯 JML 不易描述,需要与其他方法结合。JML 和规格驱动开发的核心思想是用契约代替信任,将模糊的预期转化为可检验的布尔表达式。在软件质量要求高的场景下能极大地提升可靠性和可维护性。
第一次作业建立了基础的社交关系与视频管理框架。核心功能包括用户添加、视频上传、关注/取消关注、查询互关数、双向BFS最短路径等。此时数据关系较单纯,主要是用户之间的关注与粉丝集合,以及用户与其上传视频的关联。
第二次作业引入了视频平台的完整互动体系:硬币余额、观看/点赞/转发/投币、评论系统、勋章购买、贡献榜、视频热度、视频推荐、影响力排行等。User 和 Video 的属性和方法数量爆炸式增长,频繁出现了“判断某元素是否存在于某集合”的操作。社交网络从静态演化为动态交互系统。
第三次作业深化了个性化和全局统计:用户各分区观看计数、兴趣模型、UP 影响力计算、推荐排序优化、最长下降序列查询、用户画像等。要求 User 能快速提供各类型的统计值,全局算法需要遍历大量用户或视频。性能压力扩展到批量计算和排序,大于等于 O(n²) 的实现一定会超时。
迭代带来的规格变化是代码重构的直接信号,可以通过以下几种方式察觉:
①自建大规模随机测试与计时
当某个方法耗时占比超过一定阈值,就针对其分析内部循环。这是发现 ArrayList 查找瓶颈、评论清理重复计算等问题的直接手段。
②复杂度
分析每个操作的复杂度。
③关注 JML 对模型字段的隐含访问成本
JML 中常有类似 (\sum int v; ...) 或 \forall 表达式,它们对应着需要快速求值的数据,要主动将“可以增量维护的聚合值”变为类中的字段,在变更时更新。
④迭代自身的叠加效应
很多时候单次迭代的某个方法并不慢,但所有新增方法合在一起时该结构的缺陷会被成倍放大。因此需要主动以最大规模的心态审视既有容器,而不是仅满足于当前作业新增功能。
现象:
同一用户的未观看视频列表中可能出现重复的视频ID,造成显示冗余,且在观看后无法完全清除。
原因:
User.addReceivedVideo 方法在接收到转发视频时,未检查该视频是否已存在于接收列表中。每次转发都会创建一个新的链表节点插入头部,导致同一视频被多次记录。虽然观看时 removeReceivedVideo 会通过 nodeMap 移除所有对应节点,但在未观看期间,重复条目不仅占用内存,还会让 queryReceivedUnwatchedVideos 返回重复值,且 receivedSize 计数偏大,影响后续基于该计数的逻辑。
修复方式是在添加前通过 nodeMap 判断是否已存在,若存在则不再重复添加,或仅更新位置。
现象:
当用户数量、关注关系或观看记录达到一定规模时,查询操作耗时急剧增加,导致整体运行超时。
原因:
最初使用 ArrayList 存储粉丝列表、关注列表和已观看视频集合。ArrayList.contains 需要线性遍历,时间复杂度为 O(n),在大数据量且高频调用的场景下时间超限。
改为 HashSet 后,利用哈希查找将平均复杂度降为 O(1),瓶颈消除。
现象:
短时间内对同一视频多次执行 cleanSpamComments 且使用相同的关键词,每次都要重新统计所有评论中该关键词的出现次数,CPU 时间消耗巨大,容易超时。
原因:cleanSpamComments 中的关键词计数涉及字符串匹配,本身有一定开销。最初的实现没有记录上一次清理的参数和结果,每次调用都从零开始遍历所有现存评论进行匹配统计。当同一视频被连续传入相同 keyword 时,这些计算是冗余的。
修复方案是引入 lastKeyword 和 cachedCounts 缓存:如果新调用的关键词与上次相同且评论集合未发生变化,则直接复用缓存的计数结果。
现象:
在统计评论中某个关键词的出现次数时,如果评论内容较长或关键词出现频繁,原有算法耗时过高。
原因:
最初的实现采用了 String.indexOf 循环、split 后计数,其最坏时间复杂度接近 O(n*m)(n 为评论文本长度,m 为关键词长度),当评论数量多且文本长度较大时开销变得不可接受。
改用 KMP 算法后,通过预处理模式串构建部分匹配表,将单次匹配的复杂度降为 O(n+m),整体性能显著提升。
①快速将半形式化规格翻译为可编译代码
JML 的前置、后置、信号子句可以近似一对一映射到方法的参数校验、状态更新、异常抛出。大模型能准确识别 requires 对应的 if-throw,ensures 对应的赋值或集合操作,signals 对应的异常类实例化。这大幅减少了从规格文档到手写实现之间的工作量,尤其当方法数量多达几十个时。
②自动补全异常路径和边界条件
规格中列出一个方法可能抛出的异常类型。大模型通过训练数据中类似契约的写法,能以较高概率生成合理的检查次序并自动补齐每个异常对应的输出语句。
③高效生成符合契约的正确性优先实现
在给出明确的 JML 后,大模型倾向于生成最直接满足可见行为的代码,避免因理解偏差而引入的逻辑错误,保证行为与规格严格一致。
④快速生成测试思路和断言
大模型可以阅读 JML 后列出应当测试的正常场景、边界场景、异常场景,并生成对应的 JUnit 框架,包括 assertEquals、assertThrows 的具体参数。开发者只需补充具体数据即可执行,实现“规格→测试”的快速转化。
大模型在直接生成代码时极可能忽视性能,这是规格驱动自动化中最突出的短板。
①倾向直译规格中的聚合运算
比如,如果 JML 写 ensures result == (\sum int i; ...; ...),模型可能直接生成一个 for 循环在每次查询时计算总和,而不会主动想到维护一个 totalSum 字段并增量更新,然后因为每次遍历计数导致高频调用时超时。
②默认使用最通用的容器,不自主优化
模型很难主动判断“这个集合需要频繁查找成员”等压力问题,比如通常会使用 ArrayList 存放列表,因为它只追求逻辑正确,不会评估数据规模带来的复杂度差异。
③缺少对重复计算和缓存的意识
大模型生成的代码通常每次调用都重新计算,不会引入状态缓存,因为 JML 没有描述“调用频率”或“数据稳定性”,模型只按单次调用的语义去实现。
会,容易在数据一致性上产生隐蔽缺陷。
①结构照搬规格而不考虑复用和内聚
大模型为每个新功能定义独立的数据存储,但不会主动将“观看行为”封装为统一方法,而是多个地方分别修改这些状态,导致逻辑分散,增加维护风险,架构上缺乏对聚合根或职责分离的思考。
②忽视容器间的同步一致性
比如,模型可能简单地用LinkedList实现接收列表,但不维护 nodeMap。当需求变为“同一视频可以被多次转发但只应出现一次”时,JML 本身未明确建模这种去重,模型便不会主动添加去重机制,造成数据冗余和查询错误。这种容器一致性是架构层面需要人来设计的。
③对内存和引用共享缺乏警惕
大模型可能无意中共享可变对象,例如返回内部集合的引用导致外部修改破坏不变量。
(注:大模型对于内存共享机制的漠视在Unit2的多线程作业中体现得更明显一些)
①从规格自动生成测试场景清单
向大模型提供接口的 JML 源码,要求它列出每个方法的:
②生成测试代码骨架
让模型根据场景清单生成 JUnit 测试类,包含每个测试方法的名称、初始设置、操作调用和断言。然后填充具体参数值,并调整构造数据的细节。
③利用模型补充异常断言细节
对于异常测试,模型不仅生成 assertThrows,还会建议验证异常对象中携带的 ID、年龄等数据,有助于避免仅捕获异常类型但忽略异常内容是否正确的疏漏。
①自己写 JML 时容易留下隐性假设
写作者写规格时会不自觉地把脑海中的实现细节当作公理、把自己默认的上下文当成了规格的一部分。例如,定义关注操作时,下意识认为“用户一定存在”是前置条件,但在 JML 中漏写了 requires containsUser(id1) && containsUser(id2),因为觉得这是常识。当这份规格传到下家时,对方根据自然语言描述“用户A关注用户B”,可能会补全这个条件,也可能不会。等到第二次传递,就出现了两种可能:一是主动检查用户存在性并抛异常,二是直接操作集合导致空指针。
②异常处理顺序
比如“视频投币”操作,可能触发用户不存在、视频不存在、自己给自己投、硬币不足等多个异常。我在自然语言中描述成“检查这些情况,不合格就报错”,但没有规定顺序。下一个同学还原成 JML 时,会根据自己的逻辑排列if语句,导致面对同一组错误输入,不同人的实现抛出异常的类型和顺序不同。这种边界在多次传递后会被成倍放大,最终与原始需求完全脱节。
③自然语言的模糊性稀释精确语义
自然语言具有二义性,每一次从形式化到自然语言的翻译,都会丢失一部分精确性,而重新形式化时又会被添加进解读者的个人理解,特别是涉及到集合的重复元素、元素的顺序时。比如,JML 中的 ensures (\forall int i; ... ; ...) 到了自然语言变成“所有符合条件的都满足某条件”。原本 JML 用 \sum 表达的东西,经过两道转译可能变成了简单计数。
④数据结构的隐含约束被忽略
比如我用 JML 描述一个“接收未观看视频列表”,是一个不包含重复元素的队列。在后续传递中,这个列表是否允许重复变成了一个无主之约,无人明确声明,不同人的实现就出现了分歧。
关键在于将共识转化为机械化的、可验证的契约。
①用形式化接口作为唯一的真相源
给出完整的接口,方法签名、异常列表全部确定,再配合 JML 规格。
②强制约定异常优先级和前置条件检查顺序
规格文档必须明确写出所有方法在遇到多个可触发异常的条件时,以什么顺序检查。规则一旦固定,所有组员在实现时只需遵守,可以减少传递中因顺序不同导致的行为差异。
③数据结构与复杂度要求在规格中显式声明
除了行为规格,还需要非功能规格,将“效率约束”提升为接口契约的一部分。比如,团队可以约定:所有“集合包含判断”操作必须使用散列结构。这样,即便组员各自负责不同模块,也不会有人乱改。