2024OO第三单元总结

杨汇一-22371376 学生 2024-05-19 11:58:33

1.本单元测试过程:

1.1黑箱与白箱:

  • 黑箱测试:要求测试人员站在用户的角度进行测试,给出数据输入,只要代码的输出合理即可,不需要关注内部的细节与实现。这种方法通常用于功能测试与用户角度的测试。公测与互测用的就是黑盒测试。

  • 白箱测试:需要测试人员去检查代码内部的结构有与逻辑,通过代码分析,来保证自己的数据覆盖全部情况,无逻辑错误。主要用于单元测试与集成测试,我们针对jml语言写的Junit测试也是白箱测试的思想。

1.2各种测试:

  • 单元测试:是针对代码中的最小单元进行测试,通常是函数或方法,这种测试是基本单元的测试,防止在函数实现时产生问题;

  • 功能测试:是验证系统功能是否符合需求和规格,这种测试可以帮助我们检查是否会遗漏功能;

  • 集成测试:是验证多个模块之间的交互和集成是否正常,这个测试是在单元测试的基础上,保证接口之间连接等没有错误;

  • 压力测试:是测试系统在负载情况下的性能表现,强测中很多对于性能的测试,可以让我们发现可优化的潜力;

  • 回归测试:是在修改代码或添加新功能后,重新运行之前的测试用例,确保修改不会影响原有功能的正常运行,在我们修复bug后,要保证原有的正确功能不出问题。

1.3 数据构造策略:

在写Junit测试时,主要用了随机生成的策略。还可以与特殊图结合起来。

2. 架构设计和性能问题

2.1 第九次作业

2.1.1 架构设计:

首先实现了异常类,在Count类中用静态变量和静态数组管理异常类出现的次数。并用单例模式实现count的引用。

public class Counter {
    private static HashMap<Integer, Integer> epiCount;
    private static HashMap<Integer, Integer> erCount;
    private static HashMap<Integer, Integer> pinfCount;
    //……
    private static int epi = 0;
    private static int er = 0;
    private static int pinf = 0;
    //……
    private Counter() {
        //……
    }

    private static Counter counter = new Counter();

    public static Counter getInstance() {
        return counter;
    }
    //……
}

然后在异常类的print()方法中实现次数的增加。这个单元的异常类都可以用这个方法实现。

public class MyEqualPersonIdException extends EqualPersonIdException {
    private int id;

    public MyEqualPersonIdException(int id) {
        this.id = id;
    }

    public void print() {
        Counter.getInstance().addException("epi");
        Counter.getInstance().addException(id, "epi");
        System.out.println("epi-" + Counter.getInstance().getException("epi")
                + ", " + id + "-" + Counter.getInstance().getIdException(id, "epi"));
    }
}

然后是networkpersons容器的选择,考虑到每个person都有一个独一无二的id,我选择了以id为键的Hashmap。同理,Personacquaintancevalues容器也选用以id为键的Hashmap

第九次作业涉及到图模型的方法主要有isCircle()(判断两个person是否可达),queryBlockSum(关系图的连通分支),queryTripleSum(关系图中三角关系的数量)。对此,我新建了一个类Map专门管理整个network关系图,并利用了并查集算法。并查集是一种树型的数据结构,用于处理一些不相交集的合并及查询问题。有一个联合-查找算法定义了两个用于此数据结构的操作:

  • Find:确定元素属于哪一个子集。这个确定方法就是不断向上查找找到它的根节点,它可以被用来确定两个元素是否属于同一子集。(适合用来实现isCircle()
  • Union/Merge:将两个子集合并成同一个集合。

map中的数据结构主要有:

public class Map {
    private HashMap<Integer, Integer> parent = new HashMap<>();    //存储节点的父节点,父节点的parent是其本身
    private HashMap<Integer, Integer> rank = new HashMap<>();    //存储每个节点所在树状图结构的层数(只有根节点时为0)
    private HashMap<Integer, HashSet<Integer>> graph = new HashMap<>();    //存储每个节点和其邻居节点(邻居节点不包括节点本身)
    private int blockCnt;
    private int tripleCnt;    //动态管理blockCnt和tripleCnt
    ……
}

find()方法体现了路径压缩的思想,在每次寻找节点所在树的根节点时,都将沿途的节点的父节点设置成根节点,这样可以将整个树的层数压缩至2,减小时间复杂度。而merge()方法则体现了按秩合并的思想,合并时,总是将秩较低的树的根指向秩较高的树的根。

img

img

public class Map {
    //……
    public void add(int id) {
        if (!parent.containsKey(id)) {
            blockCnt++;
            parent.put(id, id);
            rank.put(id, 0);
        }
    }

    public int find(int id) {
        int root = id;
        while (root != parent.get(root)) {
            root = parent.get(root);
        }    //找到id所在树的根节点root
        int now = id;
        while (now != root) {
            int up = parent.get(now);
            parent.put(now, root);    //将沿途的节点的父节点都更新为根节点
            now = up;
        }
        return root;
    }

    //addRelation(id1, id2, ……)
    public int merge(int id1, int id2) {
        //……
        //首先更新graph
        addTriple(id1, id2);    //计算新增的三角关系,注意要在更新graph之后
        int root1 = find(id1);
        int root2 = find(id2);
        if (root1 == root2) {
            return -1;    //若id1和id2本来就在一个树中,则不做操作
        }
        blockCnt--;    //若原来不在一个树中,合并后将联通子图数减一
        int rank1 = rank.get(root1);
        int rank2 = rank.get(root2);
        if (rank1 < rank2) {
            parent.put(root1, root2);    //将秩较低的树的根指向秩较高的树的根
        }
        else {
            if (rank1 == rank2) {
                rank.put(root1, rank1 + 1);
            }
            parent.put(root2, root1);
        }
        return 0;
    }

    //modifyRelation(id1, id2, ……)
    public void deleteEdge(int id1, int id2) {
        subTriple(id1, id2);    //注意要在更新graph之前
        //……
        //更新graph
        HashSet<Integer> visited1 = new HashSet<>();
        dfs(id1, visited1);    //利用dfs算法查找与id1可达的所有的节点(包括id1)
        for (int node : visited1) {
            parent.put(node, id1);
            rank.put(node, 1);
        }
        HashSet<Integer> visited2 = new HashSet<>();
        if (!visited1.contains(id2)) {    //若visited1不包含id2,说明此时id1和id2已经不可达,需要更新与id2可达的所有节点,同时将blockCnt加一
            blockCnt++;
            dfs(id2, visited2);
            for (int node : visited2) {
                parent.put(node, id2);
                rank.put(node, 1);
            }
        }
    }

    private void addTriple(int id1, int id2) {
        for (int neighbor : graph.get(id1)) {
            if (graph.get(neighbor).contains(id2)) {
                tripleCnt++;
            }
        }
    }

    private void subTriple(int id1, int id2) {
        //……
    }
}

2.1.2 出现的bug以及性能问题:

主要是在维护blockCnt时出现了错误,当删去边后,若两点间不可达,我忘了对blockCnt加一

public void deleteEdge(int id1, int id2) {
        //……
        if (!visited1.contains(id2)) {    //若visited1不包含id2,说明此时id1和id2已经不可达,需要更新与id2可达的所有节点,同时将blockCnt加一
            blockCnt++;
            //……
        }
    }

2.2 第十次作业

2.2.1 架构设计:

第十次作业中涉及到图模型的方法主要是queryShortestPath,由于对于性能要求比较高,我采用了bfs算法。

public int getShortestPath(int start, int end) {
        if (start == end) {
            return 0;
        }
        Queue<Integer> queue = new LinkedList<>();
        HashSet<Integer> visited = new HashSet<>();
        HashMap<Integer, Integer> pre = new HashMap<>();
        queue.offer(start);
        visited.add(start);
        pre.put(start, start);
        while (!queue.isEmpty()) {
            int current = queue.poll();
            if (current == end) {
                break;
            }
            for (int next : graph.get(current)) {
                if (!visited.contains(next)) {
                    queue.offer(next);
                    visited.add(next);
                    pre.put(next, current);
                }
            }
        }
        int count = 0;
        int current = end;
        while (current != start) {
            current = pre.get(current);
            count++;
        }
        return count - 1;
    }

对于queryBestAcquaintance,我主要利用了数据结构(容器)实现了性能提升。在Person中,额外用一个有序容器存储acquaintance。这样,在queryBestAcquaintance()时,就可以把O(n)的复杂度降低到O(1)。

public class MyPerson implements Person {
    private TreeMap<Integer, TreeSet<MyPerson>> acquaintanceByValue
            = new TreeMap<>(Collections.reverseOrder());

    //……
    private void addAcquaintanceByValue(MyPerson myPerson, int value) {
        acquaintanceByValue.putIfAbsent(value,
                new TreeSet<>(Comparator.comparingInt(MyPerson::getId)));
        acquaintanceByValue.get(value).add(myPerson);
    }

    private void removeAcquaintanceByValue(MyPerson myPerson, int value) {
        acquaintanceByValue.get(value).remove(myPerson);
        if (acquaintanceByValue.get(value).isEmpty()) {
            acquaintanceByValue.remove(value);
        }
    }

    public int queryBestAcquaintance() {
        if (acquaintanceByValue.isEmpty()) {
            return id;
        }
        return acquaintanceByValue.get(acquaintanceByValue.firstKey()).first().getId();
    }
}

queryCoupleSum()时,可以不必遍历network中所有的person,可以设置flag标记哪些person已经被找到bestAcquaintance了。

public int queryCoupleSum() {
        int count = 0;
        HashSet<Integer> flags = new HashSet<>();
        for (Integer i : persons.keySet()) {
            if (flags.contains(i)) {
                continue;
            }
            MyPerson myPerson = persons.get(i);
            int best1 = myPerson.queryBestAcquaintance();
            if (best1 == i) {
                continue;
            }
            MyPerson bestPerson = persons.get(best1);
            int best2 = bestPerson.queryBestAcquaintance();
            if (best2 == best1) {
                continue;
            }
            if (best2 == i) {
                count++;
                flags.add(best1);
                flags.add(best2);
            }
        }
        return count;
    }

MyTag中,要对getAgeMean()getAgeVar()getValueSum()所涉及的数据进行动态维护,具体方法是在类内部存储ageSumagePowSumage的平方和)和valueSum

public int getAgeMean() {
        //……
        return ageSum / persons.size()   }

public int getAgeVar() {
        //……
        int mean = getAgeMean();
        return (agePowSum - 2 * mean * ageSum + persons.size() * mean * mean) / persons.size();
    }

2.2.2 出现的bug以及性能问题:

处于直觉没有维护tag里的valueSum,而是在每次queryTagValueSum时遍历一次,这就导致在面对复杂tag的多次查询可能会超时,强测的一个数据点就在这上面卡了时间:

……
qtvs 114514 1919810
qtvs 114514 1919810
qtvs 114514 1919810
qtvs 114514 1919810
qtvs 114514 1919810
qtvs 114514 1919810
……

解决方案是动态维护valueSum,在tag里实现changeValueSum方法,以便在删减边和修改边时调用。

public void changeValueSum(int value, int id1, int id2) {
        if (persons.containsKey(id1) && persons.containsKey(id2)) {
            valueSum += value;
        }
    }

还有注意一点,在jml里两个人之间的valueSum是算了两遍的,所以taggetValueSum需要返回的是valueSum * 2

2.3 第十一次作业

2.3.1 架构设计:

要注意一点,就是queryReceivedMessages时,要涉及寻找messages的前5个消息,所以要用有序容器存储message,我选择了Arraylist

2.3.2 出现的bug以及性能问题:

还是出现在了qtvs指令上,有可能是第十次作业没有测出来,原因是在addRelation时没有维护tag里的valueSum

经过了三次作业与性能的斗智斗勇,我最终理解了jml只是对方法的前提和结果进行了约束,而中间的过程(包括性能提升等)可以让我们自由发挥,不必按照jml语言一板一眼地翻译成Java语言。

3. Junit测试方法:

主要分为数据构造和测试两个部分,需要注意以下几点:

  • 由于测试可能包含了稀疏图、密集图等,我三次作业都采用随机构造数据的方法。同时,为了测试强度尽量大,我将testNum设置成了20,然后再构造数据的过程中避免了各种异常。

  • 为了避免调用被测试方法前后对象状态不一致而带来的各种bug,我采取了影子network的办法,就是在生成数据时就完全复刻出一个oldNetwork等,然后在调用被测试方法后再比较networkoldNetwork等之间的状态。

  • 针对network里的pure方法,特别需要关注persons容器前后长度有没有发生改变。

  • 在第十一次作业中,除了构造MyEmojiMessage之外,还需要构造其他两种message

4. 学习体会:

本单元难度确实比较小,我认为所有的难度主要集中于如何提升图查询的性能以及如何针对jml书写Junit测试。在这个过程中,我学会了并查集的思想,学会了动态维护数据的思想,也学会了在什么情况下该用bfs算法,在什么情况下该用dfs算法。虽然在阅读jml语言时会眼花缭乱(找括号),但其实jml中有很多类似的实现方法,只要对这些类似的实现方法熟悉,阅读jml语言的困难就会降低。

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

301

社区成员

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

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