BUAA OO 第三单元总结

BUAA OO 第三单元总结

〇.综述

第三单元训练的主题是规格化设计。在本单元的实现过程中,笔者认为是一个“戴着镣铐跳舞”的过程:要在给定的JML的规格约束下,以灵活的数据结构实现规格给定的功能。在这个过程中,JML规格约束给出了逻辑正确性的保证,而数据结构的具体实现是程序性能的决定因素。

一.架构设计

在JML的规格约束下,大层面上的架构已被设计完备,需要我们格外关注的是存储JML中各个量的数据结构形式。下面给出笔者在三次作业中各个给定类的具体数据结构形式。

  • MyPerson类

    private HashMap<Person, Integer> acquaintance;
    private LinkedList<Message> messages;
    
    • acquaintance的key为该person的acquaintance,value为亲密值;

    • messages为用链表存储的消息。由于该类中涉及消息的头插,因此使用链表存储更加方便。

  • MyGroup类

    private HashMap<Integer, Person> people;
    private int meanSum;
    private int squareSum;
    private int valueSum;
    
    • people的key为person的id值,value为该id对应的person,依此法组织便于通过id值快速查找到对应的person;
    • meanSumsquareSumvalueSum为三个维护量,便于实现对应统计量的O(1)查询。具体的维护方式在二.性能优化部分会有详细阐述。
  • MyNetwork类

    private HashMap<Integer, Person> people;
    private HashMap<Integer, Group> groups;
    private HashMap<Integer, Message> messages;
    private HashMap<Integer, Integer> emojiMap;
    

    均为以id值为key的HashMap,便于利用id直接查询。

此外,笔者还额外构建了UnionFindEdgeGraph三个类便于图信息的存储与计算。下面对这三个类进行详细介绍。

  • UnionFind类

    private HashMap<Integer, Integer> parent;
    private HashMap<Integer, Integer> depth;
    private int cnt;
    

    维护了各个person对象的连通关系。其中parent记录了各person的父节点,这也是各节点是否连通的判断标志。

    与一般的并查集相同,核心的两个方法是find()merge()。额外地,每一次调用find()方法时,由于仅需维护连通关系,因此会将查询过程中的各节点的父节点设置为同一值。这一过程实质上将连通树的深度压缩为1,提高了查找效率。具体实现方法如下:

    public int find(int p) {
        int fa = parent.get(p);
        if (fa == p) {
            return p;
        } else {
            int newFa = find(fa);
            parent.put(p, newFa);
            return newFa;
        }
    }
    
  • Edge类

    public class Edge implements Comparable<Edge> {
        private Person from;
        private Person to;
        private int weight;
        
        ...
    }
    

    实现了Comparable接口,以权值weight作为比较大小的依据,便于在后面的Graph类中实现堆优化的dijkstra算法。实现接口重写需重写compareTo()方法:

    public int compareTo(Edge o) {
        if (this.weight < o.getWeight()) {
            return -1;
        } else if (this.weight > o.getWeight()) {
            return 1;
        } else {
            return 0;
        }
    }
    
  • Graph类

    private ArrayList<Edge> edges;
    private UnionFind unionFind;
    private HashMap<Integer, Integer> dirty;
    private HashMap<Integer, Integer> leastConnection;
    private HashMap<Integer, ArrayList<Edge>> adjTable;
    private boolean isSorted;
    

    核心在于adjTable,以key为结点、value为Edge的ArrayList的HashMap存储邻接表。邻接表的图存储形式可以方便dijkstra算法的实现。

    此外,注意到如果不向Network中添加新的relation,即不向Graph中添加新的边,现存各节点的leastConnection不会发生改变。为了在现有堆优化的dijkstra算法的基础上达到更好的时间性能,额外设置了HashMap<Integer, Integer> dirty,利用类似OS课程中“脏位”的思想标记各结点——倘若没有与之有关的新的relation加入,则直接输出之前计算出并存储好的统计量,以便有效应对大量重复query指令的压力测试。

二.性能优化

在构建数据结构时,一方面需要考虑到程序的计算性能,即构建的数据结构可以有效降低各个查询量计算方法的时间复杂度,如引入并查集等;另一方面需要考虑到程序的维护性能,即构建的数据结构能够尽可能多的维护各个查询量,以便将更多的查询方法降到O(1)的时间复杂度。

1.计算性能

主要体现在以下几个方面:

  • 选择合适的容器

    如Network中的person、group等使用HashMap存储便于实现O(1)查找,Person中的message使用LinkedList存储便于头插等

  • 构建合适的图结构

    先后构建了记录连通信息的UnionFind类、实现了Comparable接口的Edge类、实现了邻接表的Graph类,便于实现堆优化的dijkstra算法

  • 寻找合适的算法

    主要涉及最小生成树算法与最短路径算法。由于笔者的图结构主要以边集、邻接表形式存储,因此使用了kruskal算法与堆优化的dijkstra算法

2.维护性能

尽可能地维护更多的统计量,同时结合脏位的思想标记维护量的更新状态

  • ageMean、ageVar:由于涉及到整除问题,不能直接维护以上两个量。需要维护meanSum与squareSum,借助均值、方差的相关公式计算出当前的实际值即可
  • isCircle、BlockSum:借助并查集维护。在实现过程中可以适当优化,如在查找过程中压缩树的高度,具体实现方法在一.架构设计中有详细说明
  • sendIndirectMessage、LeastConnection:除了借助算法优化外,还额外设置了脏位,用于应对大量连续query指令的压力测试,具体实现方法在一.架构设计中有详细说明
  • 此外,其余诸如peopleNum等统计量可以直接调用相关容器的.size()方法

在三次作业中的第二次作业中,笔者没有很好的关注到算法性能方面的问题,导致查询量valueSum的查询方法的复杂度达到O(n²)进而被房友hack。后来通过在Group中增加维护量valueSum并在addPerson、delPerson两个方法中更新该量,完成了性能维护。

三.规格测试

1.根据JML规格构造测试数据

不难发现,JML规格中的require语句,正好对应着数据的范围要求。因此,为了更加全面地测试到程序的每一个分支,可以根据JML的require语句构造对应的数据进行测试。

而测试结果的正确性,则由JML的后置语句ensure作为判据。

由此体会到:JML规格不仅在设计层面提供了正确性的保证,更在测试层面保证了数据构造的全面性。

2.压力测试

然而,根据JML构造数据只提供了程序正确性的保证,而不能保证程序具有较好的性能。因此除了全面性的数据构造,还要考虑到大量指令的集中输入情况(如大量ap后再大量qgvs),以测试程序的性能优良。

3.随机测试

根据JML规格测试保证了程序的正确性,大量的压力测试保证了程序的性能。然而手撸数据太慢了。再辅以随机数据+对拍器进行自动评测,可以更好地对程序进行实际应用场景的测试。

四.异常处理

此外,本单元还较多地涉及到异常处理的知识。笔者简单做了如下总结:

讨论:Java异常讨论 - 第十一次作业 - 2022面向对象设计与构造 | 面向对象设计与构造 (buaa.edu.cn)

五.拓展作业

  • 生产产品

    /*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
                 (getPerson(id) instanceof Producer) &&
      			 getProducer(id).hasProduct(productId);
      @ assignable getProducer(id).productCounts;
      @ ensures getProducer(id).getProductCounts(productId) ==
      @         \old(getProducer(id).getProductCounts(productId)) + 1;
      @ also
      @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
                 (getPerson(id) instanceof Producer) &&
      			 !getProducer(id).hasProduct(productId);
      @ assignable  getProducer(id).products,
      				getProducer(id).productCounts;
      @ ensures (\exists int i; 0 <= i && i < getProducer(id).products.length;
       			 getProducer(id).products[i] == productId &&
       			 getProducer(id).productCounts[i] == 1);
      @ ensures getProducer(id).products.length ==
          		\old(getProducer(id).products.length) + 1 &&
      @   	    getProducer(id).productCounts.length ==
          		\old(getProducer(id).productCounts.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(getProducer(id).products.length);
      @         (\exists int j; 0 <= j && j < getProducer(id).products.length;
                getProducer(id).products[j] == \old(getProducer(id).products[i]) &&
      @         getProducer(id).productCounts[j] ==
                \old(getProducer(id).productCounts[i])));
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
                                                people[i].getId() == id);
      @ signals (NotProducerException e) (\exists int i; 0 <= i && i < people.length;
                                          people[i].getId() == id) && 
                                          !(getPerson(id) instanceof Producer);
      @*/
    public void produceProduct(int id, int productId) throws
                PersonIdNotFoundException, NotProducerException;
    
  • 发送广告

    /*@ public normal_behavior
      @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement) && 
      			 getMessage(id).getProuctId == productId;
      @ assignable messages;
      @ assignable people[*].messages;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) &&
                \old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length;
                messages[j].equals(\old(messages[i]))));
      @ ensures (\forall int i; 0 <= i && i < people.length &&
                !getMessage(id).getPerson1().isLinked(people[i]);
      @         people[i].getMessages().equals(\old(people[i].getMessages()));
      @ ensures (\forall int i; 0 <= i && i < people.length &&
                getMessage(id).getPerson1().isLinked(people[i]);
      @         (\forall int j; 0 <= j && j < \old(people[i].getMessages().size());
      @         people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) &&
      @         people[i].getMessages().get(0).equals(\old(getMessage(id))) &&
      @         people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1);
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (NotAdvertisementException e)  containsMessage(id) && 
                                               !(getMessage(id) instanceof Advertisement);
      @ signals (WrongAdvertisementException e) containsMessage(id) && 
      											(getMessage(id) instanceof Advertisement) &&
                                                !(getMessage(id).getProuctId == productId);
      @*/
    public void sendAdvertisement(int id, int productId) throws MessageIdNotFoundException,               NotAdvertisementException, WrongAdvertisementException;
    
  • 添加商品

    /*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
                 (getPerson(id) instanceof Customer) && !getCust(id).hasProduct(productId);
      @ assignable getCust(id).products;
      @ ensures (\forall Product i; \old(getCust(id).hasProduct(i));
      @          getCust(id).hasProduct(i));
      @ ensures \old(getCust(id).products.length) == getCust(id).products.length - 1;
      @ ensures getCust(id).hasProduct(getProduct(productId));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
      @                                         people[i].getId() == id);
      @ signals (NotCustomerException e) (\exists int i; 0 <= i && i < people.length;
      @                                  people[i].getId() == id) &&
      									 !(getPerson(id) instanceof Customer);
      @ signals (EqualProductException e) (\exists int i; 0 <= i && i < people.length;
      @                                  people[i].getId() == id) &&
      									 (getPerson(id) instanceof Customer) &&
      									 getCust(id).hasProduct(productId);
     */
    public void setPreference(int id, int productId) throws PersonIdNotFoundException,
    						NotCustomerException, EqualProductException e;
    

六.心得体会

众所周知,第三单元是面向算法的设计与构造,笔者在第三单元的帮助下巩固了一些算法知识

在经过第三单元的训练后,笔者认为:JML规格设计是在实际工程开发场景中的一大有力武器。不难发现,JML规格语言提供了程序逻辑正确性的保证。JML的书写者只需着眼于程序整体的逻辑流程的正确性,而无需局限在每一个功能的具体实现上;而JML的阅读者则如本文开篇所说“戴着镣铐跳舞”,无须过多地考虑程序的整体逻辑是否正确合理,只需构建合理有效的数据结构对规格进行具体实现。可以看到,这里的书写者和阅读者,恰好对应着实际工程开发中的不同分工。不同于自然语言的多义、模糊,JML规格语言及其严谨,可以保证交流的双方不会出现理解上的差错,使得在实际的工程开发中能够更好地进行团队协作。

此外,JML既是设计的要求,也是测试的要求。从设计角度看,JML的require语句对应着不同分支,ensure语句对应着对应的效果,这给实现者提供了明确的方向;从测试的角度看,JML的require语句对应着不同的数据约束,ensure语句对应着应有的正确结果形式,这给测试者提供了全面的数据约束与正确性判据。

只是,JML的书写和阅读就目前来看总感觉有些“过度冗余”。从最简单的视角——行数来看,有时JML的规格行数甚至超过了实际实现的代码行数。期待之后会有更加方便的JML“编码”“译码”工具~

posted @ 2022-06-06 15:37  Lingo30  阅读(21)  评论(0编辑  收藏  举报