OO第三单元总结.

OO第三单元总结

一、根据JML规格构造测试数据

  • 由于JML规格自身描述的清晰性,只要正确遵循JML规格去进行代码撰写,一些简单直白的方法只需要手动构造基础样例进行基本测试即可。
  • 针对异常的测试,这需要考虑到一些极端且易混淆的情形。比如第一次作业中的点与自己之间的一些情形:
qv 1 1 //查看一个人和它自己之间边的权值
qci 1 1 //查看一个人与它自己之间是否连通
ar 1 1 123 //不能给自己加关系,必须返回异常;同时这是id1==id2的情形,计数只能添加一次而不是两次

异常报错的先后顺序,如下列方法

 public void addToGroup(int id1, int id2) throws MyGroupIdNotFoundException,
            MyPersonIdNotFoundException, MyEqualPersonIdException{....}

有多个异常时,报错的先后顺序也可能出错。

  • JML规格描述和实际算法实现之间可能会造成TLE问题。如第二次作业中的qgvs指令可能会造成TLE,第一次作业中计算连通分量的算法,第三次作业中求最短路的算法,这些可以通过构造大量重复性数据去发现。

二、架构设计及图模型的构建和维护策略

架构设计

按照所给的JML规格即可实现大部分功能,其他新增的一些细节如下:

Count类

用于处理所有的异常总计数情形,以及建立了相应的HashMap,用以统计某个id所导致的异常的次数。在增加新的异常种类时,只需添加相应的计数变量,HashMap,还有add()方法即可,十分方便。

public class Count {
   private static int pinfAll = 0;
    private static int epiAll = 0;
    private static int rnfAll = 0;
    private static int erAll = 0;
    .......
     private static HashMap<Integer, Integer> Whos_pinf = new HashMap<>();
    private static HashMap<Integer, Integer> Whos_epi = new HashMap<>();
    private static HashMap<Integer, Integer> Whos_rnf = new HashMap<>();
    private static HashMap<Integer, Integer> Whos_er = new HashMap<>();
    ......
    public int getPinfAll() {
        return pinfAll;
    }

    public int getEpiAll() {
        return epiAll;
    }

    public int getRnfAll() {
        return rnfAll;
    }

    public int getErAll() {
        return erAll;
    }
    ......
    public HashMap<Integer, Integer> getWhoSpinf() {
        return Whos_pinf;
    }

    public HashMap<Integer, Integer> getWhoSepi() {
        return Whos_epi;
    }

    public HashMap<Integer, Integer> getWhoSrnf() {
        return Whos_rnf;
    }

    public HashMap<Integer, Integer> getWhoSer() {
        return Whos_er;
    }
    ......
    public void addpinfAll(int id) {
        pinfAll = pinfAll + 1;
        if (Whos_pinf.get(id) == null) {
            Whos_pinf.put(id, 1);
        } else {
            Whos_pinf.replace(id, Whos_pinf.get(id) + 1);
        }
    }

    public void addepiAll(int id) {
        epiAll = epiAll + 1;
        if (Whos_epi.get(id) == null) {
            Whos_epi.put(id, 1);
        } else {
            Whos_epi.replace(id, Whos_epi.get(id) + 1);
        }
    }

    public void addrnfAll(int id1, int id2) {
        rnfAll = rnfAll + 1;
        if (Whos_rnf.get(id1) == null) {
            Whos_rnf.put(id1, 1);
        } else {
            Whos_rnf.replace(id1, Whos_rnf.get(id1) + 1);
        }

        if (Whos_rnf.get(id2) == null) {
            Whos_rnf.put(id2, 1);
        } else {
            Whos_rnf.replace(id2, Whos_rnf.get(id2) + 1);
        }
    }

    public void adderAll(int id1, int id2) {
        erAll = erAll + 1;
        if (Whos_er.get(id1) == null) {
            Whos_er.put(id1, 1);
        } else {
            Whos_er.replace(id1, Whos_er.get(id1) + 1);
        }
        if (id2 != id1) {
            if (Whos_er.get(id2) == null) {
                Whos_er.put(id2, 1);
            } else {
                Whos_er.replace(id2, Whos_er.get(id2) + 1);
            }
        }
    }
    ......
    }

Edge类

用来表示邻接表里面的边,用以在第二次作业中,对prim算法进行邻接表+堆优化,减轻时间复杂度。

(而JML规格里面,MyPerson类里面的acquaintance数组和values数组其实起到了用邻接数组表示图的作用)

public class Edge implements Comparable<Edge> {
    private int toId; //被指向的顶点的id 
    private int weight; //边的权值
    private Edge next; //下一段边

    public Edge(int toid, int weight, Edge next) {
        this.toId = toid;
        this.weight = weight;
        this.next = next;
    }

    public int getToId() {
        return toId;
    }

    public void setToId(int toId) {
        this.toId = toId;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public Edge getNext() {
        return next;
    }

    public void setNext(Edge next) {
        this.next = next;
    }

    public int compareTo(Edge other) {
        if (this.getWeight() < other.getWeight()) {
            return -1;
        } else if (this.getWeight() > other.getWeight()) {
            return 1;
        }
        return this.getToId() - other.getToId();
        //如果边的权值一样,也不好返回0,就返回id作差吧
    }
}
图模型的构建和维护策略

邻接数组:利用MyPerson类里面的acquaintance数组和values数组

邻接表:利用新增的Edge类

并查集:新增一下以下数组,用于求连通分量的个数

private static int[] Father = new int[10000];

新增名为IdToNumber的Hashmap,用于表示点的id到点的编号的映射,方便用在并查集算法,prim算法,dijkstra算法之中。

    private static HashMap<Integer, Integer> IdToNumber = new HashMap<>();
    public void addRelation(int id1, int id2, int value) throws
            MyPersonIdNotFoundException, MyEqualRelationException {

        if (!contains(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (contains(id1) && !contains(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        }
        /*if (id1 == id2) {
            return;
        } */
        else if (getPerson(id1).isLinked(getPerson(id2)) || id1 == id2) {
            throw new MyEqualRelationException(id1, id2);
        }

        ((MyPerson) getPerson(id1)).getAcquaintance().add(getPerson(id2));
        ((MyPerson) getPerson(id2)).getAcquaintance().add(getPerson(id1));
        ((MyPerson) getPerson(id1)).getValue().add(value);
        ((MyPerson) getPerson(id2)).getValue().add(value);

        join(IdToNumber.get(id1), IdToNumber.get(id2)); //加入并查集中

        /*
        下面把边关系加入邻接表中
         */
        Edge edge1 = new Edge(id2, value, null);
        if (((MyPerson) getPerson(id1)).getNext() == null) {
            ((MyPerson) getPerson(id1)).setNext(edge1);
        } else {
            Edge lastEdge = ((MyPerson) getPerson(id1)).getNext();
            while (lastEdge.getNext() != null) {
                lastEdge = lastEdge.getNext();
            }
            lastEdge.setNext(edge1);
        }
        /*
        无向图相当于同一边,两个点都互相指着对方,所以再加一次
        */
        Edge edge2 = new Edge(id1, value, null);
        if (((MyPerson) getPerson(id2)).getNext() == null) {
            ((MyPerson) getPerson(id2)).setNext(edge2);
        } else {
            Edge lastEdge = ((MyPerson) getPerson(id2)).getNext();
            while (lastEdge.getNext() != null) {
                lastEdge = lastEdge.getNext();
            }
            lastEdge.setNext(edge2);
        }
        Person person1 = getPerson(id1); //先提前写出来,防止下面group每一次判断hasPerson时都要花时间遍历people数组一次
        Person person2 = getPerson(id2);
        for (Group group : groups) { /*这个就是防止qgvs指令TLE,在给两个人加边的时候, */
            if (group.hasPerson(person1) && group.hasPerson(person2)) { /*看看这两个人是否已经在同一组中 */
                ((MyGroup) group).setMyvalueSum(((MyGroup) group).getMyvalueSum() + value);
            } /*如果在,那就更新该组的valueSum  每次遍历一次组,都是O(n),不会超时*/
        }
    }

三、性能问题和修复情况

本次作业中,主要的性能问题都出现在了TLE上。

第一次作业

qbs指令:查询图中的所有连通分量的个数。

原先的算法:dfs,两层循环里面调用isCircle()函数判断任意两点是否连通,来累加连通分量个数。时间复杂度O(n^2),TLE。

 public boolean isCircle(int id1, int id2) throws MyPersonIdNotFoundException {.....}
 /*isCircle():判断两个点是否连通*/
 
 public int queryBlockSum() {
        int sum = 0;
        for (int i = 0; i < people.size(); i++) {
            int flag = 0;
            for (int j = 0; j < i; j++) {
                try {
                    if (isCircle(people.get(i).getId(), people.get(j).getId())) {
                        flag = 1;
                        break;
                    }
                } catch (MyPersonIdNotFoundException e) {
                    e.print();
                }
            }
            if (flag == 0) {
                sum += 1;
            }
        }
        return sum;
    }

改进:利用并查集算法,时间复杂度降为O(n)

用某个结点所在连通分量的根节点的编号,代表这个连通分量的编号。

查询连通分量时,遍历所有结点,如果这个结点的根节点是其自身,则说明这个结点是其所在连通分量的根节点。统计有多少个这样的结点,即为图中连通分量的个数。

同时,因为结点的id为int,范围太大,不好处理,所以建立一个HashMap<Integer, Integer> IdToNumber

其意义为:该id的点是第几个加入到图中的点。

用后者作为点的编号去指代某个点,而不是用点的id去指代某个点。

     private static int[] Father = new int[10000];
    private static HashMap<Integer, Integer> IdToNumber = new HashMap<>();
    
public MyNetwork() {
        for (int i = 0; i < 10000; i++) {
            Father[i] = i; //初始化,每个节点的根节点是它自己
        }
    }
    
     private int find(int x) {
        int r = x;
        while (Father[r] != r) {
            r = Father[r];
        }
        int i = x;
        int j;
        while (i != r) {
            j = Father[i];
            Father[i] = r;
            i = j;
        }
        return r;
    }

    void join(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            Father[fx] = fy;
        }
    }
    
public void addPerson(Person person) throws MyEqualPersonIdException {
.......
        IdToNumber.put(person.getId(), people.size() - 1); //建立从id到这是第几个人之间的映射
    }

 public void addRelation(int id1, int id2, int value) throws
            MyPersonIdNotFoundException, MyEqualRelationException {
            .............
        join(IdToNumber.get(id1), IdToNumber.get(id2)); //加入并查集中
    }

/*
    Union-Find (并查集)
     */
    public int queryBlockSum() {
        int sum = 0;
        for (int i = 0; i < people.size(); i++) {
            int tooli = IdToNumber.get(people.get(i).getId());
            if (Father[tooli] == tooli) {
                sum++;
            }
        }
        return sum;
    }

第二次作业

TLE之处:qgvs指令。

该指令意义:对于某个Group,获取其所有点之间的所有边的权值之和的2倍。

JML规格中给出的写法的时间复杂度是O(n^2),而每个组最多1111个人,对于这1111个人,如果每次qgvs指令时都重新计算result,那么会消耗大量时间,造成TLE。

改进方法:在MyGroup类里面维护一个属性valueSum,其代表的就是qgvs指令所求的result。

每次往组里加人减人,或者给组里已有的两个人添加边时,就更新一下valueSum。qgvs指令时直接返回valueSum*2的值就行了。

改进后的代码:

 //给组里已有的两个人添加边时,更新valueSum
 public void addRelation(int id1, int id2, int value) throws
            MyPersonIdNotFoundException, MyEqualRelationException {
........
        Person person1 = getPerson(id1); //先提前写出来,防止下面group每一次判断hasPerson时都要花时间遍历people数组一次
        Person person2 = getPerson(id2);
        for (Group group : groups) {
            if (group.hasPerson(person1) && group.hasPerson(person2)) {
                ((MyGroup) group).setMyvalueSum(((MyGroup) group).getMyvalueSum() + value);
            }
        }
    }
    
//往组里加人减人时,更新一下valueSum
 public void addPerson(Person person) {
 ......
        for (Person person1 : people) {
            if (person1.isLinked(person)) {
                myvalueSum += person1.queryValue(person);
            }
        }
        return;
    }

 public void delPerson(Person person) {
......
        for (Person person1 : people) {
            if (person1.isLinked(person)) {
                myvalueSum -= person1.queryValue(person);
            }
        }
        return;
    }

Group和Network有区别,后者才是传统意义上的图。Group相当于在图中圈了几个点而已,甚至点之间都不一定处于同一个连通分量中。

第三次作业

犯了个错误:

混淆了sendmessage()方法和sendindirectmessage()方法。

前者是是两个人之间必须直接有边相连,发消息,否则会报异常(RelationNotFoundException);

后者是两个人只要连通就行,不一定直接有边相连,这样发送消息。

发消息部分的代码,两个方法几乎一模一样,但是为了方便,我在sendindirectmessage()方法里面又调用了sendmessage()方法,所以老误报错,报RelationNotFoundException异常。

同时,这样导致本该发出去的indirectmessage()消息发不出去(因为去报异常RelationNotFoundException了,故发消息+从messages里面删除信息就不会执行)

导致本该发了然后删除的信息,没发也没删,下次再加入这条信息时,本该正常加入(因为前面本该已经删除了),却报异常(EqualMessageIdException)

改进:

其实sendindirectmessage()方法里面发消息的代码和sendmessage()方法里面type==0情形的代码是完全相同的,
完全复制粘贴即可,如果调用sendmessage()反而会报不必要的异常。

四、Network拓展

题目:

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

答:

首先在Network类中增加这三种数据维护:

@ public instance model non_null Advertiser[] advertisers;
@ public instance model non_null Producer[] producers;
@ public instance model non_null Customer[] customers;

选择如下三种方法接口:

  • 查询某个产品的销售额:
/*@ public normal_behavior
  @requires containsProduct(proId);
  @ assignable nothing;
  @ ensures \result == (\sum int i; 0 <= i && i < producers.length &&     producers[i].hasProduct(getProduct(proId)); 
  @ producers[i].getProductSales(proId));
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(proId);
  @*/

public /*@ pure @*/ int queryProductSalesSum(int proId) throws
        ProductIdNotFoundException;
  • 消费者增加对某产品的偏好:
/*@ public normal_behavior
  @ requires contains(customerId);
  @ requires containsProduct(proId);
  @ ensures getCustomer(customerId).likes(proId) == true;
  @*/
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(proId);
  @*/
  @ also
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundException e) !contains(customerId);
  @*/
public /*@ pure @*/void saleProduct(int customerId, int proId) throws
        ProductIdNotFoundException, CustomerIdNotFoundException;

  • 得到某产品的价格
/*@ public normal_behavior
  @ requires containsProduct(proId);
  @ ensures \result == getProduct(proId).getPrice();
  @*/
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(proId);
  @*/
public /*@ pure @*/ int ProductPrice(int proId) throws
        ProductIdNotFoundException;

五、学习体会

  • 我觉得JML规格的本质更像是一种”约定俗成的注释“。用自然语言去描述代码要求,表面上简单易懂,但是随着工程复杂性的增加,考虑到表达者和接受者不同的语言习惯,以及自然语言的变化和差异,那么理解出现偏差是难以避免的,双方势必都要花费大量宝贵的时间去交流,以达成认知的纠错和统一。为了避免不必要的麻烦,所以才会诞生出基于JML规格的契约化编程方法,通过将自己的需求翻译成JML规格这门新语言,准确严谨去表达出自己的意思。

  • 发展JML规格的目的就是形成一种严谨的,能准确无误表达出所有逻辑的统一性语言,但它也是一把双刃剑。为了消除模糊性,做到真正的严谨,JML规格同时也会变得抽象,舍弃了一定的易读性。(比如本单元第二次作业中的qlc指令,仅凭JML的描述很难搞懂其含义,而自然语言“生成该点所位于的连通分支的最小生成树”就一目了然。)同时,完全按照JML规格的描述去撰写代码,可能会造成性能的损失,实际写代码时要在理解JML规格含义的前提下,灵活变通优化。

  • 总之,没有哪一种语言能够一次性精准清晰表达出所有意思。无论JML规格再怎么完善,它的表达性仍然是有欠缺的,要和自然语言互为补充地去看。

posted @ 2022-06-06 16:26  朱睿达  阅读(28)  评论(0编辑  收藏  举报