BUAA OO U3
一、JML与测试
白盒测试:
白盒测试主要是从JML的规格出发。这也是老师上课的时候着重强调的知识点。
- 明确测试目标:
- 每个方法是否都满足所要求的规格。这也是一种契约式的编程思想。
- 是否能在任何使用场景下,类都能确保状态正确?
- 测试有效性问题:
- 需要测试哪些数据?
- 如何检验测试覆盖了多少代码成分?
- 模拟使用者对象与被测对象的交互:
- 通过被测对象提供的方法。
- 始终注意检查对象的状态。(
repok()
函数)
- 测试场景往往具有一定的实际意义
- 往往对应着功能与场景。
那么在实际代码中,如何借助JML来进行测试呢?在本次作业中,我大概进行了两种测试,举个实际的例子~
-
正确性测试:
举个稍微简单点的例子:对于下面的JML规格的描述:
/*@ public normal_behavior @ requires contains(id1) && contains(id2) && getPerson(id1).isLinked(getPerson(id2)); @ ensures \result == getPerson(id1).queryValue(getPerson(id2)); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id1); @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2); @ signals (RelationNotFoundException e) contains(id1) && contains(id2) && @ !getPerson(id1).isLinked(getPerson(id2)); @*/ public /*@ pure @*/ int queryValue(int id1, int id2) throws PersonIdNotFoundException, RelationNotFoundException;
我们需要构造测试点如下:
// 基于 exceptional_behavior 的测试: 1:传入参数 PersonId1 不存在。 2:传入参数 PersonId1 存在,PersonId2 不存在。 3:传入参数 PersonId1 不存在,PersonId2 不存在。 4:传入参数 PersonId1 存在,PersonId2 存在。并且(id1).isLinked(id2)==false // 基于 normal_behavior 的测试: 5:传入参数 PersonId1 存在,PersonId2 存在。并且(id1).isLinked(id2)==true,检测返回值是否正确。
-
时间复杂度测试:
时间复杂度测试虽然JML规格中没有明确的约束,但是往往在我们实际应用中,时间复杂度的需求是必不可少的。所以我们也需要对时间复杂度进行测试。
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length; @ (\sum int j; 0 <= j && j < people.length && @ people[i].isLinked(people[j]); people[i].queryValue(people[j]))); @*/ public /*@ pure @*/ int getValueSum();
比如上述代码,如果单纯的按JML的方法来写,很明显是会超时的,对此我们如何进行时间复杂度的测试呢?可以通过构造边界数据:
1111条ap指令,1111条ag指令,2118条qgvs指令。
二、架构分析:
梳理本单元的架构设计,分析自己的图模型构建和维护策略
从指导书的要求来看:
person:相当于图的节点
relation:连接每两个人,相当于图的边。
addRealtion:相当于图论中的addEdge。
isCircle:检查图中的两个点之间的连通性。
queryLeastConnection:最小生成树的应用,求由某个点生成的最小生成树。
sendIndirectMessage:求两个点之间最短路径。
queryBlockSum:查询这个图中连通块个数。
图与边的存储:
关于如何表达模型的边,我建了一个Edge类,用于存储person和value:
public class Edge {
private int start; // 开始顶点
private int end; //结束顶点
private int value; //权值
public Edge(int start, int end, int value) {
this.start = start;
this.end = end;
this.value = value;
}
}
在void addRelation(int id1, int id2, int value)
方法中,动态维护这个边集(图),这里我采用的数据结构是:
private LinkedList<Edge> edges; // 因为考虑到插入比较多,所以这里采用的是 LinkedList<Edge> ,并且有序插入
边的维护策略:由于采用的数据结构是 LinkedList<Edge>
,所以我直接采用的是有序插入的方法,动态的维护整个图:
// 动态维护,每次在addRelation需要添加边的时候,有序插入,维护其有序性。
Iterator<Edge> iterator = edges.iterator();
while (iterator.hasNext()) {
Edge temp = iterator.next();
valueTemp.add(temp.getValue());
}
if (edges.size() != 0) {
int flag = 0;
for (int i = 0; i < edges.size(); i++) {
if (value <= valueTemp.get(i)) {
flag = 1;
edges.add(i, new Edge(id1, id2, value));
break;
}
}
if (flag == 0) { // 讨论尾部插入的情况
edges.add(edges.size(), new Edge(id1, id2, value));
}
} else {
edges.add(0, new Edge(id1, id2, value));
}
queryBlockSum:
这个需要查询这个图中连通块个数。我第一反应是BFS,但是后来发现这样明显会超时的,于是最终采用了并查集的方法。代码量也比较少~直接看有多少个点的根节点ID等于他自己的ID就好了。
并且需要注意时间复杂度。这里如果不使用路径压缩的并查集的话基本会超时的。
public int queryBlockSum() {
int sum = 0;
for (Integer key : people.keySet()) {
if (find(people.get(key).getId()) == people.get(key).getId()) { // 判断根节点ID是否等于自己的ID。
sum++;
}
}
return sum;
}
但是,我们需要在别的方法中进行维护,下面是关于queryBlockSum
的维护策略:
在addRelation方法中:进行相关的维护:因为一旦两个点有了关系,那么他们定会属于同一个组,我们需要设置其父节点相同,这是就用到了union函数:
public void addRelation(int id1, int id2, int value) throws
PersonIdNotFoundException, EqualRelationException {
// 无关代码--------------------------------------------
((MyPerson) people.get(id1)).addRelation(people.get(id2), value);
((MyPerson) people.get(id2)).addRelation(people.get(id1), value);
this.union(id1, id2);
// 无关代码--------------------------------------------
}
// 并查集模板:稍微修改一下即可~
private int find(int id) {
if (((MyPerson) getPerson(id)).getFather() == id) {
return id;
} else {
((MyPerson) getPerson(id)).setFather(find(((MyPerson) getPerson(id)).getFather()));
return ((MyPerson) getPerson(id)).getFather();
}
}
private void union(int id1, int id2) {
int ifa = find(id1);
int jfa = find(id2);
((MyPerson) getPerson(jfa)).setFather(ifa);
}
queryLeastConnection:
queryLeastConnection:最小生成树的应用,求由某个点生成的最小生成树。
关于这个方法的架构设计呢。由于我在之前实现qbs方法的时候已经使用Edge
类和边集private LinkedList<Edge> edges;
建好了整个图,所以基于这个架构,我选择的是kruskal
算法,在我的架构上实现起来比较方便,代码量也比较少。如果我采用Prim算法,我还需要对节点进行操作,每次遍历添加一个点,这样代码无疑会变得更加臃肿,而且十分不优雅。因此,我选用了kruskal
算法。
将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。代码量也不是很多~
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
int n = 0; // 判断一共有多少个点和id是联通的
int countn = 0;
int sum = 0;
// 核心代码-----------------------------------------
Iterator<Edge> iter = edges.iterator();
while (iter.hasNext()) {
Edge edge = iter.next();
if (index.containsKey(edge.getStart())) {
int start = index.get(edge.getStart());
int end = index.get(edge.getEnd());
int ida = Tools.treefind(a, start);
int idb = Tools.treefind(a, end);
if (ida != idb) { // 说明可以连接。如果相等就不行,会成环的。
countn++;
sum += edge.getValue(); // 更新最小生成树的权值。
Tools.treeunion(a, start, end);
}
if (countn == n - 1) {
return sum;
}
}
}
// 核心代码------------------------------------------
return 0;
}
关于kruskal
的维护策略,基本在addRelation
方法的时候就已经动态维护了,上面讲述queryBlockSum()
的时候已经详细介绍了,两者基本相同。
sendIndirectMessage
sendIndirectMessage:求两个点之间最短路径。
在我实现这个方法的时候,由于network类已经接近500行了,所以我建了一个工具类DijstraAlgorithm
,把需要的信息都传入这个类中,然后调用这个类的方法dijstraAlgorithm.newDijstla()
来获取返回值。
所以,在network方法中,这个方法的代码量就少了很多了:
我把整个图
map
,以及temp
(所有连通的点的集合),还有start(开始的结点)和end(最终的结点)都作为参数传递给DijstraAlgorithm
类,调用这个类的方法得到最终的返回结果
// sendIndirectMessage 方法的核心代码:
DijstraAlgorithm dijstraAlgorithm = new DijstraAlgorithm(map, temp, start, end);
sum = dijstraAlgorithm.newDijstla();
return sum;
具体的算法是采用了堆优化,但是我在具体实现的时候发现了一个很迷惑的地方,就是采用堆优化前后竟然性能只快了一点点...并且构造了很多随机的数据,发现还是没有预想中的优化效果。
// 采用堆优化的最短路径算法:
PriorityQueue<NewEdge> que = new PriorityQueue<>();
int s = allPoint.get(source);
que.add(new NewEdge(s, 0));
dis[s] = 0;
while (!que.isEmpty()) {
int u = que.poll().getTo();
if (vis[u]) {
continue;
}
vis[u] = true;
if (u == allPoint.get(target)) {
return dis[u];
}
for (Integer key : map.get(points.get(u)).keySet()) {
NewEdge temp = new NewEdge(allPoint.get(key), map.get(points.get(u)).get(key));
if (dis[temp.getTo()] > dis[u] + temp.getW()) {
dis[temp.getTo()] = dis[u] + temp.getW();
que.add(new NewEdge(temp.getTo(), dis[temp.getTo()]));
}
}
}
return dis[allPoint.get(target)];
三、性能问题和修复情况:
本次主要涉及性能问题的有以下几个算法:
queryLeastConnection:最小生成树的应用,求由某个点生成的最小生成树。
sendIndirectMessage:求两个点之间最短路径。
queryBlockSum:查询这个图中连通块个数。
只要采用并查集,路径压缩,以及堆优化的迪杰斯特拉算法就不会超时,具体算法已经在上面的架构分析里面呈现了。
但是本单元我有一个比较隐晦的性能bug:
在存边的时候,我采用的是:LinkedList
数据结构。
private LinkedList<Edge> edges; // 因为考虑到插入比较多,所以这里采用的是 LinkedList<Edge> ,并且有序插入
但是我在访问元素的时候,写了 edges.get(i)
。在Araaylist
里面,get方法的时间复杂度是O(1),但是在LinkedList
里面,get方法的时间复杂度是 O(n)。
后来修复的时候,采用迭代器的方式遍历,而不使用下标访问,就修复了这个性能bug。
四、扩展分析:
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
-
发送广告:
/*@ public normal_behavior @ requires contains(id1) && contains(id2) && getPerson(id1) instanceof Advertiser && getPerson(id2) instanceof Customer @ ensures ((Advertiser) getPerson(id1).advType.equals((Customer) getPerson(id2).perType)) ==> (isCircle(id1, id2) == true) @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id1); @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2); @ signals (WrongTypePersonException e) contains(id1) && contains(id2) && !(getPerson(id1) instanceof Advertiser) @ signals (WrongTypePersonException e) contains(id1) && contains(id2) && (getPerson(id1) instanceof Advertiser) && !(getPerson(id2) instanceof Customer); @*/ public void sendAdvertise(int id1, int id2) throws PersonIdNotFoundException, WrongTypePersonException;
-
查询销售额:
/*@ public normal_behavior @ requires contains(id) && getPerson(id) instanceof Advertiser @ ensures \result == (\num_of int i; 0 <= i && i < people.length; @ people[i] instanceof Customer && @ (Customer) people[i].perType.equals((Advertiser) getPerson(id).AdvType)); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id); @ signals (WrongTypePersonException e) contains(id) && !(getPerson(id) instanceof Advertiser) @*/ public int querySalesValum(int id) throws PersonIdNotFoundException, WrongTypePersonException;
-
查询市场渠道:
/*@ public normal_behavior @ requires contains(id) && getPerson(id) instanceof Producer @ ensures \result == (\num_of int i; 0 <= i && i < people.length; @ people[i] instanceof Advertiser && @ (Advertiser) people[i].proType.equals((Producer) getPerson(id).AdvType)); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id); @ signals (WrongTypePersonException e) contains(id) && !(getPerson(id) instanceof Producer) @*/ public List<Person> queryMarketChannel(int id) throws PersonIdNotFoundException, WrongTypePersonException;
五、单元总结:
- 对 JML level0 有了较为深入的理解。ML是一种行为接口规格语言,提供了对方法和类型的规格定义手段。我在刚开始阅读规格的时候感觉比较吃力,但后来阅读量上去了之后,再阅读JML规格就会越来越熟练~
- 体会到了测试的重要性。这次通过和室友对拍,不仅能发现代码的正确性bug,还能通过运行时间的差异发现有些方法时间复杂度的问题。
- 在本单元我复习了图论一些算法,对并查集和堆优化相关算法也掌握的更为熟练。