2026面向对象第三次博客作业

徐子成-24371104 2026-06-01 21:01:11

OO-U3分析与总结

对JML和规格驱动开发的理解

我认为 JML 的一个重要价值在于,它把程序中的责任划分得更加清楚。
对于调用者来说,requires 告诉它在什么条件下可以调用这个方法。如果调用者没有满足前置条件,那么错误责任通常在调用者。
对于被调用方法来说,ensures 和 signals 规定了它必须完成的义务。只要前置条件满足,方法就必须保证正常结果或异常结果符合规格。
这种思想类似于“契约”。调用者和实现者都按照契约行事,程序的行为就更加可控。

规格驱动开发的思想,是先写清楚规格,再根据规格实现代码。它强调程序设计不能只靠“感觉正确”,而应当有明确的行为依据。
而JML,即 Java Modeling Language,是一种面向 Java 程序的形式化规格语言。它不是用来直接实现功能的,而是用来描述方法、类和对象应当满足的行为约束。相比普通注释,JML 更精确、更严格,可以表达方法调用前必须满足的条件、方法调用后必须保证的结果,以及方法执行过程中允许修改哪些状态。
作用类似于秦横扫六国后统一文字。

JUnit测试的经验

有了JML后,JUnit 测试不是简单地“跑几个样例看结果”,而是要根据规格系统地检查程序在正常情况、异常情况和状态变化上的正确性。
在这次围绕 JML 规格进行单元测试的过程中,我最大的体会是:测试必须从规格出发,而不是只从实现出发。以前写测试时,往往更关注某个输入能不能得到预期输出;但在 JML 语境下,方法是否正确不仅取决于返回值,还取决于它是否满足 requires、ensures、assignable、pure、signals 等完整约束。

首先,JUnit 测试要覆盖正常功能行为。
其次,JUnit 测试必须检查异常行为。
第三,JUnit 测试要特别关注状态是否被非法修改。
这是 JML 测试和普通样例测试最大的区别之一。很多方法从返回值看是正确的,但它可能偷偷改变了对象内部状态
最后,我认为 JUnit 测试的本质是:用程序化的方式把规格重新检查一遍。

分析三次作业的迭代过程

第一次作业主要是建立基础的视频平台网络模型,核心类包括 User、Video 和 Network。这一阶段的重点是把用户、关注关系、视频关系等基本对象组织起来,并按照接口和 JML 规格实现查询类方法。此时程序结构相对简单,很多方法只涉及用户是否存在、关注关系是否成立、视频是否存在等基本判断。

这一阶段最重要的经验是:先根据 JML 明确方法的契约,再决定实现方式。例如一个查询方法如果是 pure,就不能为了方便而修改内部状态;如果 JML 中要求返回某种统计结果,就不能只针对样例写特判,而要保证所有合法状态下都成立。

第二次作业是在原有系统上扩展视频平台业务,比如充值、观看、点赞、转发、投币、购买勋章、发送评论、清理垃圾评论等。

这一阶段最容易出问题的是:新状态和旧方法之间的兼容性。例如新增评论系统后,clean_spam_comments 不仅要删除评论内容,还要保证评论 id 和评论内容仍然对应;新增点赞、观看、投币后,也要防止重复计数、状态不同步、异常顺序错误等问题。

第三次作业进一步加入推荐类逻辑

例如 recommendNthUp 需要处理用户不存在、rank 非法、没有视频、候选用户不足等异常情况,还要按照分数降序、id 升序进行推荐排序。测试时不能只检查返回值,还要检查调用前后用户状态是否一致,因为该方法应当是查询/推荐性质的方法,不能偷偷修改网络状态。

发现已有方法变化,不能只看新增的方法名,而要系统比较新旧接口、JML 和业务语义。

容器变化通常来自业务语义变化。判断一个字段应该用什么容器,不能只看“能不能存”,而要看这个数据需要支持什么操作。
如果主要需求是通过 id 快速查找对象,就适合用:
HashMap<Integer, User>
HashMap<Integer, Video>
因为用户和视频通常都有唯一 id,查询频繁,用 Map 可以避免每次线性扫描。

如果主要需求是判断某个元素是否存在,比如是否关注、是否点赞、是否看过某视频,就适合用:
HashSet
因为集合包含判断是高频操作,HashSet 比 ArrayList 更适合。

如果主要需求是保持评论顺序,或者按照发送顺序遍历评论,就适合用:
ArrayList
因为评论不仅要存内容,还可能要维护 id 和内容的一一对应关系,顺序也可能有意义。

发现性能瓶颈,首先要从复杂度分析入手。每个方法都要估计它在最坏情况下会执行多少次核心操作
其次,要观察高频调用的方法。
第三,要根据输入特征构造压力测试。

分析自己程序出现过的bug

除了正确性 Bug,我的程序也出现过超时风险。比较典型的是 clean_spam_comments 和一些查询类方法。

例如在大量评论场景下,输入可能包含很多类似:

send_comment 1 1 3999 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
clean_spam_comments 1 aaaaaaab

这类数据的特点是评论很多、字符串很长、关键词与评论内容高度相似但不匹配。如果每次清理都重新扫描所有历史评论,并且每次都进行低效字符串匹配,就会非常慢。

另一个例子是 queryMutualFollowingSum()。如果每次查询都双重遍历所有用户,再调用 isFollowing() 判断关注关系,而 isFollowing() 内部又是 ArrayList.contains() 线性查找,那么整体复杂度会很高。数据规模变大后,就容易超时。

这类 Bug 的原因是:只在小样例上验证了功能,没有分析最坏情况下的时间复杂度。

后来我认识到,高频查询类方法最好不要每次都重新计算,而应该考虑增量维护。例如互相关注数量可以在关注和取关操作时同步更新,这样查询时直接返回结果即可

JML“击鼓传花”游戏的感悟

我发现了别人程序的bug,但传递过程中需求和边界并没有太大变化

今后组队编程时,首先,应该统一阅读任务书、接口和规格,最好一起整理一份需求共识文档。
其次,应该先统一接口,再分工实现。
多人协作时,最忌讳的是每个人在自己的模块里随意定义方法名、返回类型和容器结构。比较好的方式是先由全组确定一套公共接口
第三,要统一数据结构和状态维护规则。
第四,要建立统一的异常和边界处理规则。
第五,要使用版本管理和代码审查减少信息差。

...全文
18 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

308

社区成员

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

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