[BUAA OO]第三单元总结

[BUAA OO]第三单元总结

一.写在前面

在这一单元,我们主要对规格化设计重要的实现者、可以避免二义性的语言JML(Java Modeling Language)进行了一定的了解,体会到了一些规格化设计的思想。三次作业以提高对JML的阅读能力和对于算法复杂度的把控为主要目标,以社交网络为主要载体,整体难度较之前两单元稍小,让我能腾出一口气去处理越来越多的各科作业,感谢课程组

接下来,我将按照作业的问题顺序展开我的博客:

​ 数据的构造:完备性和边界性

​ 架构设计:基本图的实现与优化元素的维护

​ bug分析:性能与正确性

​ Network扩展

​ 学习体会

二.数据的构造

在本单元的自测中,我第一次作业自己写了评测机,后面两次作业则是借用朋友写的评测机对拍(感谢zxy(o*))并且辅以一些特殊数据来测试边界,虽然没有全部自己写评测,但是我对于评测数据的生成也进行了一定的思考。我认为对于作业来说,评测数据要保证完备性和边界性。

完备性就是说要把所有可能的情况都测到,这在现实中很难做到。但是由于在作业中可能的指令、异常有限,所以如果愿意肝的话还是可以做到的,这一点非常重要,可以在很大程度上保证你的正确性(尤其是在本单元)。因为人往往会不自觉地联想、按照经验办事,在自己构造数据时就会去构造自己认为“难”的数据,而忽略了一些“显然”的数据,但是往往自己认为显然的事情,电脑不认为是显然的,这就会导致漏情况,最终会因为没考虑一些简单的因素而出错。比如对于JML阅读的不认真或者代码的一个疏漏就可能会报错异常、少加括号等问题的出现。所以在构造评测机的时候一定要把所有的指令、异常都测一遍。

在保证了完备性的基础上,我们还需要保证数据的边界性,就是需要构造数据测到数据的边界。这个边界有两重含义,一重是限制的边界,一重是性能的边界。限制的边界需要我们仔细阅读指导书和JML规格,将其中的类似于1111、各种变量可能疏漏的、反直觉的定义都记下来,一方面阅读自己的代码查验,一方面构造数据时要针对性地构造相应的数据,比如1111就要针对地插入1112个。性能的边界则是要求我们根据指导书给出的时间限制和阅读JML找到的高复杂度方法来构造数据来试着让自己的程序“超时”,进而检验我们对于方法的实现是否满足了作业对于时间复杂度的要求。构造方法则是尽可能让每次调用该方法都进行尽可能多的循环,针对性地构造一些链图、稠密图、完全图等等。

本单元hack到三处bug,一处是完备性没有做好导致的异常错判,两处是边界性没有做好导致的qgvs时间复杂度过高。

三.架构设计

不难发现,本单元是一个类似于社交网络的图,Network是图,Person是结点,relation是边,按照JML规定的方法写就可以构建出一个这样的图,区别仅在于容器的选择,为了快速查询,我基本上都使用了HashMap数据结构,只是由于massage强调放入的顺序,所以我使用了List。

不过,在基础的社交网络图之上,为了实现算法的优化,我新增了一些变量,比如为了实现最小生成树,维护排序边集private ArrayList<ArrayList<Integer>> sortedLine = new ArrayList<>();,主要是在ar方法中新增了对新边的插入方法insertNewLine

public void insertNewLine(int id1,int id2,int value) {
        ArrayList<Integer> newLine = new ArrayList<>();
        ......
        newLine.add(id2);
        newLine.add(id1);
        newLine.add(value);
        if (sortedLine.size() == 0) {
            sortedLine.add(newLine);
        } else {
            int low = 0;
            int high = sortedLine.size() - 1;
            if (value <= getLineValue(0)) {
                sortedLine.add(0,newLine);
            } else if (value >= getLineValue(high)) {
                sortedLine.add(newLine);
            } else {
                //小心死循环 和 二分的条件
                while (true) {
                    //二分查找
                }
                sortedLine.add(high,newLine);
            }
        }
    }

为了记录上次计算出的ageMeanageVar,我采用了四个变量ageMean、isAgeMean、ageVar和isAgeVar来记录值和是否可用,(其实通过查询熟人来优化会很快,不过我是先用了信号量,就懒得改了)。

当然还有为了方便查询统一连通分量的并查集jointSearchSet,在ap时要将该人加入维护并查集。

在为了优化而新增变量时,一定要记得维护变量的正确性,要理清这个变量和图中元素的关系,从而找到该变量应该改变的操作并在其中正确地修改变量。

三.bug分析

本次作业由于在实现时已经考虑到了性能的问题并采用了助教推荐/讨论区推荐的算法,所以在性能上并没有出现问题,正确性也由对拍进行了保证,所以在最后的评测中没有出现问题,不过在对拍的过程中确实有两个bug改了很久,让我记忆犹新。这个bug出现在ageMean的计算中,是因为读指导书时不认真,将先加再除写成了先除再加,导致整数的保留出现问题。由于只有在数量较大的时候才会触发该bug,所以在测试数据达到几千条的时候才会出现该bug,debug就十分困难,不过还好该bug触发时都有ageMean,所以查看一会之后确认了位置就解决了。第二个bug出现在emojiMessage处,是我没有维护好新增的变量,在删除emojiMessage时忘了删除对应的我新增的HashMap<Integer,Integer> messageEmojiId中的元素,结果导致与emojiMessage有关的指令会出错,但是指令中没有直接测试该容器的,所以在绕了很多弯之后才发现问题居然出现在这里。这个bug可是de了足足两个小时,原因就是因为在sendMessage里没有维护messageEmojiId枯了

发现的他人bug共三个,一个是异常处理错误,一个是qgvs时间超了,可见自测数据的全面性和边界性十分重要。

下面我将具体介绍一下这三次作业为了改善性能所写的三个主要算法:并查集,最小生成树,最短路径Dijkstra。

public class JointSearchSet {

    private HashMap<Integer,Integer> parent = new HashMap<>();
    private HashMap<Integer,Integer> rootHeight = new HashMap<>();

    public JointSearchSet() {}

    public Integer numOfRoot() {
        return rootHeight.size();
    }

    public void addNode(Integer id) {
        parent.put(id,id);
        rootHeight.put(id,1);
    }

    public Integer find(Integer initialNode) {
        int node = initialNode;
        if (parent.get(node) == null) {
            addNode(node);
        }
        while (node != parent.get(node)) {
            ...
        }
        return node;
    }

    public void unionNode(int initialNode1,int initialNode2) {
        int root1 = find(initialNode1);
        int root2 = find(initialNode2);
        if (root1 == root2) {
            return;
        }
        if (rootHeight.get(root1) < rootHeight.get(root2)) {
            ...
        }
    }

    public boolean isConnected(int node1,int node2) {
        return Objects.equals(find(node1), find(node2));
    }

}

并查集是为了记录图中连通分量而存在的,在加入点(Person)时要将其也加入并查集,并查集会进行运算后更新自己,在使用时比较根节点即可。在并查集的实现过程中,我使用了递归下降和启发式合并。简单而言,前者是在搜索时将树变低,后者是在加入时选择较低的树构建。

 public static int kruskal(Integer createdLineNum,ArrayList<ArrayList<Integer>> sortedLineSet) {
        JointSearchSet kruskalJointSet = new JointSearchSet();
        int valueSum = 0;
        int countLines = 0;
        for (int i = 0;i < sortedLineSet.size();i++) {
            if (countLines == createdLineNum) {
                break;
            }
            int point1 = sortedLineSet.get(i).get(0);
            int point2 = sortedLineSet.get(i).get(1);
            if (!kruskalJointSet.isConnected(point1,point2)) {
                valueSum = valueSum + sortedLineSet.get(i).get(2);
                kruskalJointSet.unionNode(point1,point2);
                countLines++;
            }
        }
        return valueSum;
    }

最小生成树的算法我使用了kruskal,由于我自己维护了排序边集,所以计算时十分方便。

public static int dijkstra(int startId, int finalId, HashMap<Integer, Person> people,
                               JointSearchSet jointSearchSet) {
        HashMap<Integer,Integer> marked = new HashMap<>();
        HashMap<Integer,Integer> distance = new HashMap<>();
        for (Integer personId : people.keySet()) {
            if (jointSearchSet.isConnected(personId, startId)) {
                marked.put(personId, 0);
                distance.put(personId, -1);
            }
        }
        return dijkstraExe(startId, finalId, distance, marked, people);
    }

    public static int dijkstraExe(int startId,int finalId,HashMap<Integer,Integer> distance,
                                  HashMap<Integer,Integer> marked,
                                  HashMap<Integer, Person> people) {
        PriorityQueue<Node> priQueue = new PriorityQueue<>();
        priQueue.offer(new Node(startId, 0));
        distance.put(startId, 0);
        while (priQueue.size() != 0) {
            Node minNode;
            do {
                minNode = priQueue.poll();
                if (minNode.getPersonId() == finalId) {
                    return minNode.getCost();
                }
            } while (marked.get(minNode.getPersonId()) == 1);
            marked.put(minNode.getPersonId(), 1);
            HashMap<Integer, Integer> linkedEdges =
                    ((MyPerson)people.get(minNode.getPersonId())).getAcquaintances();
            for (Integer adjacent : linkedEdges.keySet()) {
                ...
        }
        return distance.get(finalId);
    }

最短路径我采用了Dijkstra,并且使用了堆优化,因为本题中的数据限制,所以会有相当一部分数据为稀疏图,堆优化的效果还是相当明显的。虽然听说PriorityQueue<Node>会比自己实现的小根堆慢,但是已经很快了,我也就懒得改了

四.NetWork扩展

假设出现了几种不同的Person

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

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

从题目的要求来看,我认为应该新增六个接口和相应的异常类PurchaseDismatchException,AdvertiseDismatchException,ProductIdNotFoundException,Advertiser、Producer、Customer继承Person,advertiseMessage、purchaseMessage继承Message,Product不继承已有接口。

Product中记录产品的信息:id、所需金钱数目等

advertiseMessage是Advertiser给Customer发送的消息类型,用来告知对方可购买的产品

purchaseMessageAdvertiser发送给Producer的消息类型,用来告知对方Customer要购买的产品

Advertiser需要新增一个广告产品列表变量,记录广告的产品;新增一个addAdvertiseProduct方法,更新广告产品列表;

Producer需要新增一个可生产产品列表,记录可生产的产品;新增setProduct方法,修改可生产的产品;

Customer需要新增一个偏好列表,记录自己的偏好;新增待处理广告HashMap ,记录收到的广告及其广告商,以待Network调用exeAdvertise方法时处理;新增setPriority方法,修改自己的偏好;新增judgeProduct方法,判断自己是否要购买Advertiser的广告内容;

Network需要让自己的sendMessagesendIndirectMessage能够发送新增的消息,能够新增产品,能够修改Advertiser、Producer、Customer中的产品列表,能够查询某Product的销售额、销售路径等等。

此外,还可以根据需要增添一些设定,比如一个产品只有一个生产商可以生产等方便实现,也可以不增添,不过那样就需要别的规则进一步规范。

JML规格:

对新增消息的发送

/*@ public normal_behavior
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @          (\exists int i; 0 <= i && i <= \old(getMessage(id)).getPerson2.exeAdvertiser.length - 1; \old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1)
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @ (\forall int i; 0 <= i && i < \old(getMessage(id)).getPerson2.\old(exeAdvertiser).length;
      @         (\exists int j; 0 <= j && j < \old(getMessage(id)).getPerson2.exeAdvertiser.length;\old(getMessage(id)).getPerson2.exeAdvertiser[j].equals(\old(getMessage(id)).getPerson2.\old(exeAdvertiser)[i])));
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @          (\exists int i; 0 <= i && i <= \old(getMessage(id)).getPerson2.exeAdvertiser.length - 1 && \old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1);(\forall Product j;\old(getMessage(id)).getPerson1.advertiseProducts.contains(i);\old(getMessage(id)).getPerson2.exeProducts[i].contains(j)))
      @ ensures (\forall int i;0 <= i && i < \old(getMessage(id)).getPerson2.exeAdvertiser.length;(\exists j;0 <= j && j < \old(getMessage(id)).getPerson2.\old(exeAdvertiser).length && \old(getMessage(id)).getPerson2.\old(exeAdvertiser)[j].equals(\old(getMessage(id)).getPerson2.exeAdvertiser[i])) ==> (\old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1) ==> \old(getMessage(id)).getPerson2.exeProducts[i].equals(\old(getMessage(id)).getPerson1.advertiseProducts.addAll(\old(getMessage(id)).getPerson2.\old(exeProducts)[j])) && !(\old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1) ==> \old(getMessage(id)).getPerson2.exeProducts[i].equals(\old(getMessage(id)).getPerson2.\old(exeProducts)[j])))
      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @ (exists int i;0 <= i && i < producers.length;produces[i].equals(\old(getMessage(id)).getPerson2()))
      @ \old(getMessage(id)).getPerson2().getProfis() =  \old(getMessage(id)).getPerson2().(\old(getProfits())) + ((PurchaseMessage)\old(getMessage(id))).getAllProductProfits()
      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @ successSellMessages.contains(\old(getMessage(id)))
      @ (\forall Message i;(\old)successSellMessages.contains(i);successSellMessages.contains(i))
      @ also
      @ public exceptional_behavior
      @ signals (AdvertiseDismatchException e) containsMessage(id) && (getMessage(id).getType == 1 || (getMessage(id).getType == 0 && (!getMessage(id).getPerson1 instanceof Advertiser || !getMessage(id).getPerson2 instaceof Customer)
      @ signals (PurchaseDisMatchException e) containsMessage(id) && (getMessage(id).getType == 1 || (getMessage(id).getType == 0 && (!getMessage(id).getPerson1 instanceof Advertiser || !getMessage(id).getPerson2 instaceof Producer)
      @*/
public void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException,AdvertiseDismatchException,PurchaseDisMatchException;

设置某Person的Priority

/*@ public normal_behavior
  @ requires contains(id1)
  @ ensures type == 1 ==>
  @ getPerson(id1).getPriority().contains(product)
  @ (\forall Product i;getPerson(id1).(\old)getPriority().contains(i); getPerson(id1).getPriority().contains(i))
  @ ensures type == 0 ==>
  @ !getPerson(id1).getPriority().contains(product)
  @ (\forall Product i;getPerson(id1).getPriority().contains(i); getPerson(id1).(\old)getPriority().contains(i))
  @ ensures type != 0 && type != 1 ==> 
  @ assignable \nothing;
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(id1);
  @*/
public void setPersonPriority(int id1, int type, Product product) throws
    PersonIdNotFoundException;

查询某产品的销售额

/*@ public normal_behavior
  @ requires containsProduct(id)
  @ ensures result == (\sum Message i;successSellMessages.contains(i) && ((PurchaseMessage) i).getPerchaseProducts().contains(id);getProduct(id).getProfits())
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(id);
  @*/
public int queryProductProfis(int id) throws 
    ProductIdNotFoundException;

五.学习体会

经过本单元的学习,可以明显感受到我对于JML的阅读能力有了一个较为显著的提升,不过我对于JML这一语言还是有许多疑惑,比如就我个人感觉而言,JML基本上不会让事情变得更简单,而是让事情变得更复杂,比较经典的例子就是最小生成树算法的JML,就是把很容易懂的一个概念翻译的晦涩难懂,不过我也知道由于我的水平比较低、眼界不开阔,可能看不到JML的优势,所以JML到底是优是劣,还要留待以后检验了。

此外,在本单元的学习中,我初步接触了契约式编程和防御式编程。简单来说,前者与JML类似,先验条件满足==>后验条件满足,程序员只需要考虑先验条件,其他可能情况一概不管;后者则是要综合考虑所有可能的输入,尽可能保证无论在什么条件下程序都不会崩溃。我认为这两者更多的是一个概念的提炼,在实际的编程中应该结合自己程序的实现要求、实现目的综合考量、综合使用。

总而言之,本单元是比较轻松的一个单元,一方面是经过了半个学期的训练,面向对象的思想和java的编程能力都有了一定的提升;另一方面,本单元的内容确实难度稍低。接下来就是OO的最后一个单元了,并且也是本学期的最后一个月,希望我能在疫情留校期间保持状态,努力学习,给大二画上一个不后悔的句号。

posted @ 2022-06-06 12:30  Jack_rbkd  阅读(15)  评论(0编辑  收藏  举报