275
社区成员
发帖
与我相关
我的任务
分享目录
(1)、根据该图构建并查集(实现对于isCricle方法的性能优化)
(2)、关于queryTripleSum以及queryCoupleSum方法的性能修改
(3)、关于queryShortestPath方法的性能修改
本文将以笔者在面向对象第三单元的三次作业中的思考感悟为基础,对2025年OO第三单元的JML规格的理解与实现做简单的分享,欢迎指出问题与交流思路。
针对代码中最小的可测试单元(如函数、方法、类)进行测试,验证单个代码单元的功能是否正确。(在本单元主要是通过Junit编写测试方法进行单元测试)
验证系统功能是否符合需求规格(用户视角),测试整个功能模块或用户流程(如发送消息、支付)。(通过编写测试方法通过比较经厉功能实现前后的相关对象的属性变化以及对规格的满足是否符合要求)
测试多个模块或服务组合后的交互是否正常,发现接口错误、数据流问题或系统间兼容性问题。(构建大规模全覆盖的复杂数据样例,综合测试项目的功能集成)
通过超负荷请求测试系统的极限性能和稳定性,找出系统崩溃的临界点,验证高负载下的错误处理能力。(对于复杂度较高的方法构建重复的数据测试样例以检测项目的处理上限,关注极端条件)
在代码修改后,重新执行已有测试用例以确保原有功能未被破坏,防止修复Bug或新增功能引入新的问题。(每次修改代码之后重复进行上述各种的测试)
对于基础数据的样例的构造有以下四种类型:
(1)、正向数据:符合预期的合法数据(如正确的用户名/密码),以测试项目最基础的功能。
(2)、反向数据:非法、异常或边界数据(如超长字符串、特殊字符、空值),以测试异常抛出以及方法对于异常情况的处理。
(3)、边界数据:临界值(如最小/最大值、空集合、零值),以测试项目对于边界值的正确处理。
(4)、性能数据:大规模数据集(如压力测试中的10万条记录),以综合测试项目的总体功能与性能。
其中前三种数据基本都是手动构造的数据,而第四种数据由测评机随机生成。且四种数据还可以通过组合策略,既能覆盖核心逻辑,又能高效发现边界和性能问题。
由于这次作业的指导书中的大部分与代码有关的规则与功能都体现在JML规格中,可能由于JML的表达形式更适合大模型的理解,所以让大模型通过这些JML规格生成各个方法的java代码的准确性相比直接用语言文字描述的更高,但是随着功能复杂程度的提高,大模型生成的模版也会有一些细节上的错误,但是在大体的规格理解上大模型的帮助还是比较大的。
对于不同场景下的复杂任务,要使大模型适应这些不同且复杂的工作场景,则需要:
(1)、明确当前的任务背景。 (2)、明确大模型可以利用以及分析的初始条件。 (3)、明确大模型处理过程的基本模式与限制。 (4)、明确大模型要完成的任务结果。

构建:本单元所构建的网络以每个Person为节点,每两个Person之间的关系为边,value为边的权值,每个Person都有一个领接表集合属性。
维护:该项目的功能中有查询两个节点是否可达、寻找两个节点之间的最短路径、该网络的TripleSum以及CoupleSum的数量等等,故对于该图的维护变得尤为重要:
该并查集可以实现删除边的功能,可以动态维护社交网络。
public class DisjointSet {
private final HashMap<Integer, Integer> pre;
private final HashMap<Integer, Integer> rank; // 在“按秩合并”里用到
private int blockNum = 0;
public DisjointSet() {
this.pre = new HashMap<>();
this.rank = new HashMap<>();
}
public void add(int id) {
if (!pre.containsKey(id)) {
pre.put(id, id);
rank.put(id, 0);
blockNum++;
}
}
public int find(int id) {
int rep = id; //representation element
while (rep != pre.get(rep)) {
rep = pre.get(rep);
}
int now = id;
while (now != rep) {
int fa = pre.get(now);
pre.put(now, rep);
now = fa;
}
return rep;
}
public int merge(int id1, int id2) {
int fa1 = find(id1);
int fa2 = find(id2);
if (fa1 == fa2) {
return -1;
}
int rank1 = rank.get(fa1);
int rank2 = rank.get(fa2);
if (rank1 < rank2) {
pre.put(fa1, fa2);
}
else {
if (rank1 == rank2) {
rank.put(fa1, rank1 + 1);
}
pre.put(fa2, fa1);
}
blockNum--;
return 0;
}
public void removeEdge(int x, int y, HashMap<Integer,Boolean> setX,
HashMap<Integer,Boolean> setY) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
return; // 本来就不连通,无需处理
}
// 判断是否需要分裂
if (!setX.containsKey(rootX) || !setY.containsKey(rootX)) {
for (Integer node : setX.keySet()) {
pre.put(node, x);
rank.put(node, 0);
}
rank.put(x, 1);
pre.put(x,x);
for (Integer node : setY.keySet()) {
pre.put(node, y);
rank.put(node, 0);
}
rank.put(y, 1);
pre.put(y,y);
blockNum++;
}
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
例如将关系网中的TripleSum的值作为Network在addRelation与modifyRelation时动态维护,并作为Network的一个属性。
for (Person person : persons.values()) {
if (person.getId() != p1.getId() && person.getId() !=
p2.getId() && person.isLinked(p1) && person.isLinked(p2)) {
tripleSum++/--;
}
}
以及对Tag中的ageVar与ageMean,Person中的maxValue以及BestId,OfficialAccount中的bestCon做类似的动态维护(主要是为了提升性能避免产生O(n^2)的时间复杂度)。
一个方法的规格只是表明了该方法的前置条件、可改变的内容(副作用)、过程中满足的要求以及返回的结果,但是并没有明确指出具体实现方法(举个例子:一个简单的排序方法的规格,只要求把一个非空的集合按照某一规定的要求按一定次序排列,结果返回排序好的集合,满足该规格的实现方法有许多中,你可以使用冒泡排序,也可以使用快排,也可使用选择排序或插入排序等等),所以规格与实现是分离的。
原本是按顺序暴力遍历出一个可达的路径以证明两节点可达,后改为构建以及动态维护并查集,并使用并查集判断是否可达。
前者原本是三个循环嵌套遍历出结果O(n^3)改为在添加关系时动态维护TripleSum的值O(n),后者原本是一个调用了qba后O(n^2)的遍历改为动态维护bestId的值将其变为O(n)。
一开始我使用的是Dijkstra算法来寻找最短路径,而后发现由于本单元的图中的路径权值均为1,故使用该算法时对于路径长度的缓存的循环是无意义的,反而会浪费时间,故改为广度优先算法(BFS)来实现该方法。
//BFS
HashMap<Integer,Integer> distances = new HashMap<>();
ArrayList<Integer> queue = new ArrayList<>();
for (Integer id : persons.keySet()) {
distances.put(id,Integer.MAX_VALUE);
}
distances.put(id1, 0);
queue.add(id1);
while (!queue.isEmpty()) {
int currentId = queue.remove(0);
Person current = persons.get(currentId);
for (Person neighbor : current.getAcquaintance().values()) {
int neighborId = neighbor.getId();
if (distances.get(neighborId) > distances.get(currentId) + 1) {
distances.put(neighborId, distances.get(currentId) + 1);
// 如果找到目标节点,直接返回结果
if (neighborId == id2) {
return distances.get(neighborId);
}
// 将邻居加入队列继续搜索
queue.add(neighborId);
}
}
}
利用好规格以完成测试方法
明确测试目标:避免过度测试或遗漏关键场景。
提高测试覆盖率:覆盖所有需求分支(如正常流程、异常流程)。
验证代码一致性:确保实现代码严格遵循规格。
通过断言(assertEquals)确认代码输出与规格一致,副作用与规格规定的一致,使得检测过的方法基本没有正确性问题,完美符合规格要求。
经过第三单元的学习,我已经初步掌握了规格语言的构成,用法以及与代码实现的对应和不同,学会了对规格的读写以及代码与规格的转换,并在该过程中进一步深化了对于层次化的理解,JML在一定程度上避免了自然语言描述方法要求时的二义性,具有很大的使用及测试意义。本单元也加深了我对于部分算法的了解,主打一个在代码中与时间赛跑。