2026OO第三单元总结

欧阳晨-24373470 2026-05-28 21:28:10

题目回顾

本单元主要考察根据 JML 给出的规格编写 Java 代码的能力和编写JUnit的能力。

第一次作业

模拟网络社区的用户关注关系网络及视频互动,维护关注关系的整个图。

add_user id(int) age(int) name(String) 
upload_video uploaderId(int) videoId(int)
follow_user id1(int) id2(int)
unfollow_user id1(int) id2(int)
watch_video userId(int) videoId(int)
query_received_unwatched_videos userId(int)
query_up_followers_age_ratio upId(int)
query_mutual_following_sum
query_shortest_path id1(int) id2(int)

第二次作业

升级在线视频平台模拟系统,新增硬币经济体系互动(点赞/投币/转发/评论) 以及 粉丝勋章 功能。

add_user_coins userId(int) coins(int)
upload_video uploaderId(int) videoId(int) type(String)
like_video userId(int) videoId(int)
coin_video userId(int) videoId(int) amount(int)
forward_video userId(int) videoId(int) followerId(int)
send_comment userId(int) videoId(int) commentId(int) comment(String)
clean_spam_comments videoId(int) keyword(String)
query_best_contributor upId(int)
query_most_popular_video type(String)
purchase_medal userId(int) videoId(int) amount(int)
queryLongestDecSeq

第三次作业

进一步升级在线视频平台模拟系统,新增智能推荐系统

recommend_video user_id(int)
recommend_Nth_up user_id(int) rank(int)
query_most_influential_up type(String)
query_user_profile user_id(int)
queryGlobalBestContributor

架构设计

存储方式

​ 本单元是依据规格来实现相关代码,规格只是描述该方法所需要满足的限制和达到的效果,并未明确指出未达到相应的效果所实现的相关细节。如果一味按照规格表层的描述来翻译的话,很可能在一层层的循环中超时。

​ 选取恰当的容器就十分重要了。对于 Unit3 而言含 id 对象使用 HashMap<Integer, Object> 较为合适,在查找上就只有O(1)的时间复杂度了。对于 receivedVideo 成员,有从头部插入,删除特定成员,询问前五个内容等操作,由于需要高效函数和维护其内部顺序,理论上使用 LinkedList 较为合适。

​ 本次作业中,所有核心对象(用户、视频)均具有唯一 ID,因此使用HashMap<Integer, Object> 进行存储是最直接且高效的选择:

  • **Network 中维护 usersvideos**:分别用 HashMap<Integer, User>HashMap<Integer, Video>,实现 O(1) 的查找、添加、删除。
  • User 内部容器
    • followingfollowerswatchedVideoslikedVideos 等均以 HashMap<Integer, User/Video> 存储,保证快速判断是否存在及访问。
    • receivedVideos 需要支持头部插入删除指定元素查询前 5 个。理论上 LinkedList<Video> 可直接满足操作,但 LinkedList 底层为双向链表,节点在堆中离散分布,CPU 缓存不友好,遍历和删除的常数较大。更优的实现是:自建 Node 类(包含 Video 对象、前驱后继指针),再用 HashMap<Integer, Node> 维护 ID 到节点的映射,同时维护链表头指针。这样删除任意节点可 O(1) 完成,遍历前 5 个也仅需 O(5) 时间。
    • contributions 记录每个贡献者的投币总数,用 HashMap<Integer, Integer>,并在更新时动态维护 bestContributorIdbestContribution
  • Video 内部容器
    • comments 使用 LinkedHashMap<Integer, String>,既保证按插入顺序迭代,又能通过 ID 快速判断重复和删除。cleanSpamComments 需要遍历并删除满足条件的评论,使用 Iterator 安全删除。

图的存储和维护

社交网络本质是一个有向图(关注关系):

  • 每个 User 是图中的一个节点,出边为 following(关注的人),入边为 followers(粉丝)。
  • followUserunfollowUser 时,同时更新双方的两个 HashMap,复杂度 O(1)。
  • 对于 queryShortestPath,直接对 following 做 BFS,时间复杂度 O(N + E),其中 N 为用户数,E 为关注边数。
  • 对于 queryLongestDecSeq,需要沿着 following 方向找到最长的年龄严格递减链。通过记忆化 DFS 实现,每个节点只计算一次,总复杂度 O(N + E)。

方法高效实现

大部分方法(如 addUseruploadVideowatchVideo)本身为 O(1) 或 O(k)(k 为粉丝数),无需额外优化。

1. queryMutualFollowingSum

优化策略:在 Network 中维护一个 mutualCount 变量,初始为 0。

  • followUser(id1, id2) 成功时,如果 id2 已经关注了 id1(即存在互关),则 mutualCount++
  • unfollowUser(id1, id2) 成功时,如果 id2 仍然关注 id1(取关后互关消失),则 mutualCount--

这样查询方法直接返回 mutualCount,O(1) 时间,避免每次 O(N²) 的全量统计。

2. queryBestContributor

优化策略:在 User 中维护 bestContributorIdbestContribution

  • 每当 beCoined(sponsorId, amount) 被调用时:
    • 更新 contributions 映射。
    • 获取该赞助者的新总贡献值 newValue
    • newValue > bestContribution(newValue == bestContribution && sponsorId < bestContributorId),则更新最佳贡献者信息。
  • 查询时直接返回维护好的 bestContributorId,O(1) 时间,避免每次遍历所有贡献者。

3. cleanSpamComments

优化策略:由于评论长度 ≤ 50,关键词长度 ≤ 8,总评论数可能较多(一个视频可有数千条评论),直接对每条评论调用 String.contains 或自己实现 KMP 均可。但需要注意:

  • 必须严格遵循 JML:只删除包含 keyword 的评论,且返回删除数及被删评论中 keyword 的最大出现次数。
  • 对于 keyword 为空字符串的情况,按照规格,应删除所有评论,返回最长评论的长度 + 1。
  • 遍历时使用 Iterator 边遍历边删除,避免 ConcurrentModificationException。使用 KMP 统计出现次数,复杂度 O(总字符数),在数据范围内完全可行。

4. recommendVideorecommendNthUp

  • **recommendVideo**:需要遍历所有视频,计算 computeVideoScore(user, video)。由于视频总数 V ≤ 10000,每次推荐 O(V) 可接受。但若该指令被频繁调用,可考虑缓存每个用户的推荐结果(需注意用户观看历史变化时无效缓存)。
  • **recommendNthUp**:需要遍历所有用户(N ≤ 10000),对每个未关注的候选 UP 主计算 computeUpScore(需遍历 7 种类型,常数小),然后排序 O(N log N)。整体 O(N log N + 7N) 在 10⁴ 规模下安全。

5. queryLongestDecSeq

优化策略:使用记忆化 DFS。

  • 定义 dfs(user) 返回从该用户出发的最长递减链长度。
  • 递归计算其所有关注者(年龄更小)的链长,取最大值 +1。
  • 使用 HashMap<Integer, Integer> 缓存结果,避免重复计算。

6. receivedVideos 的删除操作

原代码中使用 LinkedList 存储 receivedVideos,删除时调用 removeIf,时间复杂度 O(L)。优化方法:

  • 自定义双向链表节点,同时用 HashMap<Integer, Node> 维护 ID 到节点的映射。
  • 当需要删除视频时,通过映射找到节点,直接修改前后指针,O(1) 完成。
  • 头部插入也是 O(1)。查询前 5 个只需从 head 遍历 5 步。

这一优化在收到大量视频(理论上一个用户可能收到所有视频)且频繁观看(即删除)时尤为重要。

类图

img

出现问题和修复情况

​ 在第二次强测时,因为忽略了receivedVideos可能存在来自follower上传的视频和other forward来的视频是同一个,在watchVideo是采用receivedVideos.remove(video.getId());就只会删除第一个ID符合的视频,会遗漏其余同名的视频,所以需要修复为**receivedVideos.removeIf(videoId -> videoId == video.getId());**,至于产生的性能不足,已在上文receivedVideos 的删除操作中讨论。

JML和规格驱动开发的理解

JML 的核心价值

JML(Java Modeling Language)是一种形式化的规格描述语言,它用逻辑断言来精确描述 Java 类/方法的行为契约

规格驱动开发的实践流程

  • 逐条翻译 JML 为代码

    • @requires → 前置条件判断,抛出对应异常。

    • @assignable → 明确哪些对象/属性可修改,避免副作用污染无关容器。

    • @ensures → 用代码实现后置状态。复杂的 \old 表达式需要提前保存旧值。

    • @signals → 异常类型与触发条件。

  • 设计内部容器与辅助变量:JML 中的 instance model(如 followingcontributions)是抽象模型,需要选择合适的数据结构实现并保证不变量(如 contributors.length == contributions.length

JUnit测试的经验

针对 JML 的测试策略

课程要求为特定方法(如 queryMutualFollowingSumcleanSpamCommentsrecommendNthUp)编写 JUnit 测试,主要的思考方向有

  • 测试 requires 子句:分别构造满足/不满足前置条件的输入,验证正常执行或抛出正确的异常。
  • 测试 ensures 子句:用断言验证后置状态是否与规格一致。对于复杂表达式,可拆解为多个断言。
  • 测试 assignable 子句:在方法调用前深拷贝受影响的对象(或利用课程组提供的 strictEquals 方法),调用后对比未被 assignable 列出的属性是否真的没有变化。
  • 测试 pure 属性:调用前保存所有相关属性的快照,调用后断言快照完全一致(对象内容、容器元素等)。
  • 测试 signals 子句:使用 assertThrows 验证特定异常被抛出,并检查异常消息或类型。

常见陷阱与应对

  • 引用比较 vs 内容比较:JML 中的 equals 通常需要比较对象内容,而不仅仅是 id。课程组提供的 strictEquals 方法就是用来做深比较的。测试时不要直接用 == 比较 UserVideo 对象。
  • 容器顺序的不可假设性:规格中未规定 receivedVideos 的迭代顺序时,测试不应依赖顺序。但某些方法(如 queryReceivedUnwatchedVideos)明确要求“最新的在前”,此时需要验证顺序。
  • 浮点数精度queryUpFollowersAgeRatio 返回 double[],比较时应使用 assertEquals(expected, actual, 1e-6)
  • 异常链的测试:某些方法可能调用其他方法抛出异常,JUnit 应测试原始调用者抛出的异常类型异常抛出顺序

大模型使用经验

在 Unit 3 的开发中,我尝试使用大模型辅助理解规格和生成基础测试。

  1. 大模型的优势
    在遇到长达几十行的嵌套 \forall 和 \exists时,大模型能帮你快速理解其意义。
    在写 JUnit 测试时,大模型能帮我迅速写出极端构造脚本,用于检测程序正确性。
  2. 大模型的弊端
    一般不要让大模型帮你直接把 JML 翻译成现成代码。大模型比较死板,可能会无视架构/容器,看到 JML 里的 users.length,可能会直生成一个数组或 ArrayList,而忽视实际的查找需要,导致时间过长。
    也可能会无视性能: 看到 \sum 嵌套,大模型可能会直接生成三层 for 循环。比如计算全网互关对数,极大可能会引发TLE。

研讨课JML“击鼓传花”游戏的感悟:

你是否发现了自己/别人的JML的bug? 在传递过程中,需求,边界是否发生了变化? 今后多人组队编程时,你认为怎么做才能统一所有人对任务需求实现方法的理解?采用什么措施(或者指定规则)可以减少组内成员间的信息差?

在自然语言和JML语言交替传递的过程中,我们组出现了无中生有、逻辑充要性问题等问题,具体体现如下:

  • 自然语言描述部分存在明显疏漏。表述存在口语化歧义,没有精准覆盖边界场景,同时遗漏部分前置条件和异常行为说明,规格内容不完整,为后续信息偏差埋下隐患。
  • 在形式化转换中,受上一轮歧义文本影响,加之自身JML掌握不扎实,出现多处错误。存在量词使用混乱、\old关键字误用、assignable可赋值范围界定不准确等问题,且后置条件书写片面,只定义了核心功能,未约束非目标元素的状态,导致JML契约不完整。
  • 纯形式化的JML代码逻辑晦涩、嵌套复杂,在逆向翻译时遗漏了可修改范围、异常约束等关键信息,还再次使用模糊口语表述,进一步加剧了信息失真。
  • 第四轮二次编译时,经过多轮传递的文本已经严重偏离原始需求,最终写出的JML逻辑漏洞较多。

实践建议:

1. 契约的“双重表达”

  • JML + 自然语言注释:JML 提供精确的逻辑约束,但可读性较差。每个方法上方同时用自然语言总结功能、边界条件和副作用,便于快速理解。
  • 示例输入/输出:对关键方法提供典型调用和预期结果,这比纯逻辑描述更直观。

2. 评审机制

  • 规格评审会:在编写实现之前,团队集体 review 所有 JML 规格。评审清单包括:
    • 所有 @requires 是否完整?
    • 所有 @signals 是否互斥且覆盖所有异常路径?
    • 量化表达式(\forall\exists\num_of)是否正确?
    • 是否有隐式假设未写入规格?
  • 交叉实现测试:A 写规格,B 写实现,C 写测试。三方独立,最后比对。

3. 统一编码与规格风格

  • 容器顺序规范:如果方法依赖顺序(如“最新在前”),必须在规格中明确声明。
  • 异常优先级:规定当多个异常条件同时满足时,抛出顺序(例如 UserIdNotFoundException 优先于 SelfSubscriptionException)。
  • \old 的使用规则:约定在非 pure 方法中,\old 只能用于基本类型或不可变对象,避免深层拷贝带来的性能与歧义。
...全文
13 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

307

社区成员

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

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