BUAA_2025_OO_Unit3单元总结

刘舒童-23373306 2025-05-19 20:36:00

u3是JML规格设计单元,我们要阅读规格完成一个社交网络图模型的构建与维护。在这个单元中,既考查了我们对JML规格相关知识的掌握,要要求了一些数据结构和算法知识的综合运用能力。

目录

  • 测试过程
  • 对单元测试、功能测试、集成测试、压力测试、回归测试的理解
  • 单元测试
  • 功能测试
  • 集成测试
  • 压力测试
  • 回归测试
  • 数据构造策略
  • 大模型辅助
  • 作业架构分析
  • 性能问题及bug修复,规格与实现分离的理解
  • 性能问题及bug修复
  • 规格与实现分离的理解
  • Junit测试
  • 学习体会

测试过程

对单元测试、功能测试、集成测试、压力测试、回归测试的理解

单元测试、功能测试、集成测试、压力测试和回归测试是软件质量保障的关键环节,各自覆盖不同维度和阶段。

单元测试

对软件中最小的可测试部分(如函数和方法)进行测试,旨在验证这些最小代码单元按照预期工作。一般是白盒测试:依赖代码内部逻辑,通常由开发者编写。具有隔离性
而且通常高频运行,作为开发阶段的“安全网”。它的目的是验证代码逻辑正确性,快速定位低级错误(如边界条件、异常处理)。例子是本单元中每次与中测一同出现的特殊 JUnit 测试,均要求对特定的方法进行测试。

功能测试

验证程序的功能是否满足需求的测试。它一般是黑盒测试:不关注内部实现,只检查输入输出是否符合预期。是场景驱动,覆盖用户故事或业务流程(如登录、支付)。目的一般是确保功能按需求交付,避免偏离用户预期。在本单元中,我认为每行合法的输入其实都对应一种要求的功能,所以功能测试包含在了我的黑箱测试当中,因为每个功能是否正确实现不仅表现于对应行的输出是否正确,也可以通过后续有耦合的功能的对应输出进行判断。即如果实现不当,错误功能的外在表现会体现在此条及之后的输出之中。但正如上文叙述的,黑箱测试并不能保证发现功能的所有错误。

集成测试

在单元测试之后进行,目的是验证不同模块或组件如何协同工作。其实我实践的黑箱测试更偏向于一种系统测试,其在一定程度上也能完成集成测试的目的,即从结果上验证了功能模块之间协同工作的效果。要完整地实践集成测试,应该首先全面地完成单元测试,然后再仅针对模块间的交互行为进行测试的设计。然而本单元的作业交互程度其实并不高,即交互行为较少、各功能之间较为独立,实现集成测试的性价比不如系统测试。

压力测试

验证软件在极端工作条件下的行为,如高负载或有限资源。在本单元中,内存空间较为宽裕,对软件性能的验证主要在于运行时间,需要压力测试来验证程序的实现是否足够高效。它关注系统崩溃点、恢复能力(如高并发、大数据量),模拟极端场景。目的是识别性能瓶颈(如数据库锁、线程池不足),优化系统扩展性。

回归测试

在软件发生更改后进行的测试,以确保现有功能仍然按预期工作。在本单元中,我对回归测试的实践在于将写完的程序打包为 jar 文件,再次投入到前一次作业的评测机中进行测试,以确保没有修改原有的规格。

数据构造策略

我在学长往届的数据构造器基础上搭建了适合本单元的数据构造器。
具体过程主要就是按格式生成各项指令即可,十分清晰明了。

def relation_instr(instr_name, id1, id2, value):
    instr = instr_name
    instr += ' '
    instr += str(id1)
    instr += ' '
    instr += str(id2)
    instr += ' '
    instr += str(value)
    instr += '\n'
    return instr

一些有用的策略是,可以减小id的范围,然后增大指令的条数,这样就可以保证进行充分的查询,而不是看似有大量数据,其实很多输出都是异常,不一定能够测试到每一处细节。

更进一步,我搭建了多条测试数据自动化测试的代码,便于进行大量测试。值得关注的是,通过我搭建的评测的几万条数据测试的代码在互测中还是有被发现出问题的情况。这充分说明我们要客观认识搭建的评测机的威力到底如何,并且不能完全依赖评测机,在必要的时候要充分熟悉自己的代码逻辑,进行代码走查。

大模型辅助

1.遇到长度很长,复杂难懂的规格。我会让大模型进行阅读并解析,给出实现思路。这样我能对这段规格的大意有所了解,就便于我仔细去阅读理解规格。
2.大模型可以给出一些数据结构、算法的优化建议

提问时,我们要“循循善诱”,逐步得到自己想要的最佳结果。

下面给出一些具体建议:

通过明确模型能力边界,构建精准的交互场景,例如:"作为具备JML建模经验的Java开发专家,请协助完成社交网络系统的规格解读与实现工作。当前需要分析的JML约束如下:..."
关键点在于提供完整的方法签名、关联规格片段及异常定义,避免模型因信息缺失产生臆测。
复杂问题的分步拆解
规格解析:要求模型逐层解构复合逻辑(如嵌套量词\forall/\exists的组合语义),并通过示例对比加深理解:"请将JML中的ensures子句转化为自然语言描述,并举例说明路径查找失败的情形。"
实现引导:采用"框架生成→逻辑补全"的递进模式。首先生成符合JML签名的基础结构(含异常抛出点),再针对核心算法(如网络关系中的BFS遍历)要求补充关键代码段。
反馈驱动的迭代优化
当模型输出存在逻辑漏洞时,采用针对性修正指令:"当前生成的代码未处理PersonId不存在的情况,请参考JML中的requires约束补充异常处理"。通过引入测试用例反推模型改进,例如:"若p1_id等于p2_id,这段代码会违反哪条JML约束?"

在此值得提醒的是,我感觉到自己对大模型的依赖在逐渐加深,很多日常工作也都交给大模型完成了。但是一定要留出自己独立思考的空间,知识是无穷无尽的,在当今知识爆炸的时代,自己的独立思考,自己的头脑才是自己的。

作业架构分析

下面给出本单元经过三次迭代最终得到的架构图。

img

在本单元中,我们维护了一个社交网络图模型。其中所有的元素都在network类构造出的一个实例中。除此之外,有Person,OfficialAccount,Article,Message等内容。

下面我们对具体图模型构建与维护的具体策略作以分析:

1.Network类:

private HashMap<Integer,Person> persons = new HashMap<>();//保证persons中元素互异

private HashMap<Integer,OfficialAccount> accounts = new HashMap<>();

private HashSet<Integer> articles = new HashSet<>();

private HashMap<Integer,HashMap<Integer,Tag>> tags = new HashMap<>();//这里要注意不同人tag的id有可能一样

private HashMap<Integer,MessageInterface> messages = new HashMap<>();

private HashSet<Integer> emojiIds = new HashSet<>();  // 存储表情 ID

private HashMap<Integer,Integer> emojiHeats = new HashMap<>();

其中大多用HashMap,是为了便于从id直接找到对应的对象,加快速度。
除了常规的存储之外,值得一提的是这里对tags的设置。分析如下:
由于有querytagvaluesum的要求,如果按照常规办法遍历的话$O(n^{2})$会超时,因此我采用局部维护每一个tag的valuesum变量。这就需要在network全局维护一个tags数组,每当有边的权值修改或者addpersontotag或者delpersonfromtag,我们就调用

public void allmodifytags(Person p1, Person p2,int value) {
        for (Person p : persons.values()) {
            int id = p.getId();
            for (Tag t : tags.get(id).values()) {
                t.modifyRelation(p1,p2,value); 
                } 
            } 
        }

来实现对每一个tag的维护。这里值得注意的是,不同person所拥有的tag的id有可能重复,因此直接采用HashMap<Integer,Tag>tags就会出问题。最终我采用这样的方式存储tags

HashMap<Integer,HashMap<Integer,Tag>> tags

同时,我们还在network中维护了并查集,实现了对isCircle连通块查询的支持。

import java.util.HashMap;
import java.util.HashSet;

public class DisJoint {

    private HashMap<Integer, Integer> parent = new HashMap<>();
    private HashMap<Integer, Integer> rank = new HashMap<>();

    private HashMap<Integer, HashSet<Integer>> edge = new HashMap<>();

    public void addEdge(int id1, int id2) {
        ......
    }

    public void removeEdge(int id1, int id2) {
        edge.get(id1).remove(id2);
    }

    public void initReset() {
        parent.clear();
        rank.clear();
    }

    public void remakeSet(int id) {
        //......
    }

    public void reset() {
        for (int id1 : edge.keySet()) {
            for (int id2 : edge.get(id1)) {
                merge(id1, id2);
            }
        }
    }

    public void add(int id) {
        parent.put(id, id);
        rank.put(id, 0);
    }

    public int find(int id) {
        // 不能用递归,使用循环
        int re = id;
        //......
        return re;
    }

    public void merge(int id1, int id2) {
        int root1 = find(id1);
        int root2 = find(id2);
        if (root1 == root2) {
            return;
        }
        int rank1 = rank.get(root1);
        int rank2 = rank.get(root2);
        if (rank1 < rank2) {
            parent.put(root1, root2);
        } else if (rank2 < rank1) {
            parent.put(root2, root1);
        } else {
            parent.put(root1, root2);
            rank.put(root1, rank1 + 1);
        }
    }

    public boolean isConnected(int id1, int id2) {
        if (!parent.containsKey(id1) || !parent.containsKey(id2)) {
            return false;
        }
        return find(id1) == find(id2);
    }
}

一些核心的优化策略:
1.遇到变量查询
尽量不要暴力遍历,可以采用日常维护+脏位设置延迟更新来处理。
2.优化数据结构:
在Person中有一个receivedArticles变量。要求:
快速实现在头部插入元素
允许存储重复元素
快速删除所有符合要求的元素
输出前5个元素
分析上述要求,开始经过思考我决定采用LinkedHashMap这种方法去支持快速的增删。但是hw11要求允许重复元素,这种有HashMap的数据结构就失效了。于是我直接自己维护了一个数据结构,用ArticleNode来存储一个articleid。


public class ArticleNode {
    private int articleId;
    private ArticleNode next;
    private ArticleNode prev;

    public ArticleNode(int articleId) {
        this.articleId = articleId;
    }

    public int getArticleId() {
        return articleId;
    }

    public void setArticleId(int articleId) {
        this.articleId = articleId;
    }

    public ArticleNode getNext() {
        return next;
    }

    public void setNext(ArticleNode next) {
        this.next = next;
    }

    public ArticleNode getPrev() {
        return prev;
    }

    public void setPrev(ArticleNode prev) {
        this.prev = prev;
    }
}

最终对receivedArticles采用如下的结构进行存储:

private HashMap<Integer, ArrayList<ArticleNode>> receivedArticles = new HashMap<>();

下面给出增删的方法:

public void addArticle(int articleId) {
        ArticleNode newNode = new ArticleNode(articleId);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.setNext(head);
            head.setPrev(newNode);
            head = newNode;
        }
        if (receivedArticles.containsKey(articleId)) {
            receivedArticles.get(articleId).add(newNode);
        }
        else {
            ArrayList<ArticleNode> newNodes = new ArrayList<>();
            newNodes.add(newNode);
            receivedArticles.put(articleId, newNodes);
        }

    }

    public void removearticle(int articleId) {
        if (receivedArticles.containsKey(articleId)) {
            ArrayList<ArticleNode> nodeToRemoves = receivedArticles.get(articleId);
            Iterator<ArticleNode> it = nodeToRemoves.iterator();
            while (it.hasNext()) {
                ArticleNode next = it.next();
                if (next.getPrev() != null) {
                    next.getPrev().setNext(next.getNext());
                }
                else {
                    head = next.getNext();
                }
                if (next.getNext() != null) {
                    next.getNext().setPrev(next.getPrev());
                }
                else {
                    tail = next.getPrev();
                }
                it.remove();
            }
        }
    }

就可以满足上述要求,实现高效的增删以及输出了。

关于最短路:
我参考了往届学长的一些比较成熟高效的方法,在此不再赘述。

性能问题及bug修复,规格与实现分离的理解

性能问题及bug修复

第一次作业遇到了灭顶之灾,原因是一处异常的条件写错了,实在是太不应该。并且关于isCircle的并查集删边的实现也有些问题。

第二次作业在性能方面有一个TLE,我对性能进行了优化。具体采用的数据结构和算法在前面已经分析过。互测还被测出全局tags的存储问题,即没有考虑到不同Person所拥有的tag的id是有可能相同的。这一内容在前面已经分析过了。

第三次作业的强测和互测没有出现bug。

规格与实现分离的理解

规格是开发者与系统之间的契约,明确定义了“组件应做什么”,而非“如何做到”。
例如,JML中的requires定义了前置条件,ensures约定了后置条件,而实现只需满足这些契约即可,无需暴露内部细节。
规格层:关注功能性正确性(如输入输出的数学约束、状态变迁规则),通常使用形式化语言(如JML、TLA+)或自然语言描述。
实现层:聚焦工程可行性(如算法选择、性能优化、资源管理),依赖具体编程语言和技术栈。
这种隔离使得同一规格可对应多种实现(如不同算法版本、跨平台适配),提升系统灵活性。

规格与实现相分离,举个例子,在本单元中,规格对某个对象仅仅用int[]来表示,但实际实现可能采用各种各样的数据结构来表示这一成员变量。

并且,规格给出了前件和结果的要求,这就给了我们选择实现算法的自由,才有了优化性能的空间。比如Tag.getAgeMean/Var,JML只要求返回正确的值。就可以从实时计算(实现简单,但查询可能慢)优化为缓存+增量更新(实现稍复杂,但查询快)。

Junit测试

本单元提出了一种特殊的测试方法,即编写 JUnit 测试,对课程组提供的错误代码按照规格进行检测。这类测试保证其他方法均正确实现,故可以调用其他正确方法来进行社交网络的构造和相关规格检验对象的获取。在实现 JUnit 测试的过程中,我认为最重要的就是两个相辅相成的要点:保证数据生成的覆盖率和断言的全面性。

数据生成的覆盖率:没有全面的数据,就难以触发错误代码对规格的违反。所以在 JUnit 测试中,应该注意对要测试方法的所有有关属性进行充分的改变,在足够多样的环境中尝试进行断言测试。
断言的全面性:要保证对 JML 规格进行全面的翻译,不能落下任何一条语句。

JUnit检验代码实现与规格一致性的效果:

高覆盖率:遵循JML指导可以系统地覆盖各种正常路径、边界条件和异常路径,从而提高测试覆盖率。
精确性:JML的精确性使得测试目标非常明确。测试不再是凭感觉,而是有据可依。
自动化:JUnit使得这些基于规格的测试可以自动化执行,极大提高了回归测试的效率。
早期发现问题:能够在开发早期就发现实现与规格之间的偏差,降低修复成本。
提升代码质量:为了通过基于JML的严格测试,开发者必须更仔细地思考代码逻辑和边界情况,从而间接提升了代码质量。

学习体会

本单元的难度相较于多线程单元有一个明显下降,但是也埋下祸根。让我在第一次作业中掉以轻心,没有进行充分测试,直接再hw9中爆0。后面不敢掉以轻心,充分测试,才在后面两次作业没有出现大问题。
我认为JML在一些关键的场景下是有重要的作用的,但在很多日常情况下稍显冗余,似乎不如自然语言描述来的那么方便。当然,在对代码正确性和逻辑严密要求很高的情况下,对JML的需求则是必须的,对JML的书写也有助于架构人员本身对问题的理解更加深入。

同时,我感觉JML的相关内容应该在形式化验证等科学领域有其重要作用,值得我们进一步研究。
对于课程建议,希望对 JUnit 的测试中可以提供更多信息的反馈,或者设计单独的评测,我觉得靠同学间的探索和交流来猜测错误代码有什么问题的现象对于学习如何进行测试没有什么意义。
同时建议可以提醒大家检查自己的异常检测,别因为一些奇怪的问题强测爆0.在此还是感谢OO课程组的全体助教和老师为课程建设的辛勤和伟大付出与贡献!

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

275

社区成员

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

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