BUAA OO U3

一、JML与测试

白盒测试:

白盒测试主要是从JML的规格出发。这也是老师上课的时候着重强调的知识点。

  1. 明确测试目标:
    • 每个方法是否都满足所要求的规格。这也是一种契约式的编程思想。
    • 是否能在任何使用场景下,类都能确保状态正确?
  2. 测试有效性问题:
    • 需要测试哪些数据?
    • 如何检验测试覆盖了多少代码成分?
  3. 模拟使用者对象与被测对象的交互:
    • 通过被测对象提供的方法。
    • 始终注意检查对象的状态。(repok()函数)
  4. 测试场景往往具有一定的实际意义
    • 往往对应着功能与场景。

那么在实际代码中,如何借助JML来进行测试呢?在本次作业中,我大概进行了两种测试,举个实际的例子~

  1. 正确性测试:

    举个稍微简单点的例子:对于下面的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,检测返回值是否正确。
    
  2. 时间复杂度测试:

    时间复杂度测试虽然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:吃瓜群众,不发广告,不买东西,不卖东西
  1. 发送广告:

        /*@ 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;
    
  2. 查询销售额:

        /*@ 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;
    
  3. 查询市场渠道:

    /*@ 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;
    

五、单元总结:

  1. 对 JML level0 有了较为深入的理解。ML是一种行为接口规格语言,提供了对方法和类型的规格定义手段。我在刚开始阅读规格的时候感觉比较吃力,但后来阅读量上去了之后,再阅读JML规格就会越来越熟练~
  2. 体会到了测试的重要性。这次通过和室友对拍,不仅能发现代码的正确性bug,还能通过运行时间的差异发现有些方法时间复杂度的问题。
  3. 在本单元我复习了图论一些算法,对并查集和堆优化相关算法也掌握的更为熟练。
posted @ 2022-06-06 17:32  乌拉圭的袋鼠  阅读(30)  评论(0编辑  收藏  举报