目录
架构设计
算法设计
Graph 类
新建辅助类 Graph ,在其中维护邻接表的无向不带权图的数据结构:
HashMap<Integer, HashSet<Person>> acquaintanceMap;
在该类中:
- 使用以路径压缩和按秩合并进行优化的并查集算法来判断两个节点是否连通,实现 isCircle() 方法
- 使用DFS算法来维护删除关系后原节点的所有连通节点
- 使用以双向遍历进行优化的BFS算法来查询无向图两节点之间不带权的最短路径长度,实现 queryShortestPath() 方法
- 同时,在该类中实时维护连通分支 blockSum 。当修改节点或者边的时候,若 blockSum 发生变化,则及时维护
MyTag 类
设置 ValueSum 实时维护子图中所有边的权重之和
注意:在加人/删人(加入/删除节点)时也要维护 ValueSum 。同时,在加关系时,应先维护 person 的关系,再维护这个人所有熟人的 Tag 的 ValueSum ;而删关系时, 应该将二者的顺序反过来!
MyNetwork 类
在加关系、删关系、改关系时,实时维护 tripleCount (三元环数目),并重置涉及到节点的 BestAcquaintance (最佳关系者)。实时维护图结构、和 Tag 中的 ValueSum
异常处理
由计数类 Count 与十二个具体的异常类配合完成。
其中,Count 类中这样实现初始化:
private int totalCount = 0;
private HashMap<Integer, Integer> idCount = new HashMap<>();
public Count() {
}
而具体的异常类,先 生成一个静态的 count 实例
private static Count count = new Count();
private int id;
public MySomethingException(int id) {
this.id = id;
count.add1TotalCount();
count.addIdCount(id);
}
这样可以实现 十二个不同的异常处理类,共用同一个计数器。
总体
代码量竟然 3k+ 了啊啊啊!(虽然好多都是课程组给的代码和JML描述
测试过程
各种测试
黑箱测试
黑箱测试即把程序看作一个黑盒子,在完全不考虑内部实现细节的情况下,对程序提供的输入输出接口进行测试
测试者往往通过观察整个程序是否有符合预期的输出判断程序的正确性
常见的各类评测机都是典型的黑箱测试
这样的测试可以避免惯性思维,对程序的功能进行强测,但往往很难定位bug的具体位置
白箱测试
白箱测试指的是测试者对程序的逻辑结构,算法,内部如何执行等信息完全了解,以此基础进行测试
测试者通过特定的输入,观察程序各语句的执行结果、各分支跳转等是否符合预期
已知错误后进行的debug,Junit的编写,即为典型的白箱测试
其重要指标是测试的覆盖率(函数、分支、代码行的覆盖率)
单元测试
指对软件中的最小可测试单元进行检查和验证,通常针对单个函数或类的方法进行测试
单元测试的优势在于,在 bug 产生之初,就尽早发现并修复,为后续整个工程的调试节省时间。同时,单元测试也可能较为繁琐,消耗人力和时间
集成测试
基于单元测试的基础,一般在单元测试之后进行
集成测试主要指对于模块之间接口与交互的测试,保证多个单元聚合的正确性
功能测试
用于验证软件系统的各项功能是否按预期工作,测试人员会根据需求规格编写测试用例,并通过输入不同的数据来验证程序是否满足设计要求
功能测试一般采用黑盒测试,目的是确保软件能够按照需求正常运行,并能够正确地处理各种情况
压力测试
在程序运行的极限情况下,对程序进行测试。模拟多用户、高并发等场景,保证程序在压力下的稳定运行。测试的重点是在当前程序实现的情况下时间复杂度和空间复杂度的极大情况
本次强测中,几乎全部是删除关系的数据点,包含大量qcs
,qts
,qbs
指令的数据点属于压力测试
回归测试
指在修改旧代码之后,对原有的功能进行重测,确保新增或者修改的代码没有引入新的错误,没有产生不符合预期的副作用
在 bug 修复环节,不仅仅测试出错的数据点,而是对所有的强测点均进行重测,这属于回归测试
数据构造策略
- 采用大量随机数据,尽量覆盖所有指令,所有语句分支,检测程序的正确性和运行时间
- 手动构造边界情况,进行压力测试
构造数据时,先构造一个基础的图,包含大量的节点和边,然后着重测试复杂度高的指令和新增的指令
Bugs
出现过的bug
- 在 DFS 维护并查集根节点的时候,忘记维护实行DFS的那个节点的根节点
- 在 Mytag 类中, addPerson() 和 delPerson() 方法未判断该 Tag 中是否已经有这个人,因为题目的要求不保证这一点
- 在 modifyRelation() 时,维护 ValueSum 的先后顺序没注意
- 如下
性能问题
- 在第一次作业中,我在 MyPerson 类中,将JML中描述的 acquaintance 和 value 定义为了 ArrayList ,导致对 ArrayList 中指定元素的查找,增加,删除等操作的复杂度由 O(1) 变成了 O(n) 。
- 在第二次作业中,我的 getPerson 方法,明明已经实现了 persons 的 HashMap 容器,却依然使用遍历而不是直接用 get() 方法来获得 person ,导致时间复杂度由 O(1) 变成了 O(n) 。
规格与实现的分离
规格的正确保证了代码逻辑的正确性,而面对不变的需求,可能有多种实现方式
规格从结果出发,描述了一个方法执行前后改变了什么,确保什么不变。而实现者不需要考虑整个工程的逻辑设计,只需要根据结果反推过程,同时考虑实现的效率(时间空间复杂度),颇有“带着镣铐起舞
”的感觉
规格与实现分离的做法有利于团队协作:架构师负责给出规格,而编程者则在规格下进行具体功能的编写,可以大大提高协作效率。
同时,这样的分离还降低了系统内部各个模块之间的耦合性。通过规格定义明确的接口和约束条件,不同模块之间不需要了解彼此的具体实现细节。提高了代码的健壮性和可靠性
Junit测试
一般来说,规格包含三个部分:前置条件,副作用范围,后置条件。这三者都需要测试到
Junit的编写只需要按照每个 ensures 进行测试,保证满足规格中的所有条件即可
此外,在构造测试样例的时候,要考虑到边界情况,充分测试
本单元学习体会
本单元的主题有些挂牛头卖狗肉 ,实际上考察了JML的阅读理解和图算法两大块内容
通过本单元的学习,我复习并掌握了许多常见的图算法,学会了阅读JML,对契约式编程有了一定的了解,可谓收获颇丰
同时,本单元的debug环节,让我对JAVA数据结构的效率,时间复杂度的理解有了进一步的体会