BUAA_OO_Unit3 基于JML规格的设计总结

BUAA_OO_Unit3 基于JML规格的设计总结

一、综述

​ 面向对象课程的第三单元的主题是基于JML规格的设计与测试。本单元的作业背景是实现简单的社交网络,包括NetWorkGroupPersonMessage等元素,通过添加图算法和异常类,来实现查询和异常处理功能。本次作业已经通过JML规格给出了主体架构,意在考察对JML规格的理解和代码实现以及测试能力。

二、作业分析

1. Homework 9

1.1 UML类图

1.2 分析

1.2.1 架构设计

​ 本次作业主要实现MyGroupMyNetworkMyPerson三个类,实现简单社交关系的模拟和查询,学习目标为 入门级JML规格理解与代码实现。

Network是顶层的社交网络,即顶层图结构(无向图)。其中每个Person对象为图的点,Person之间的acquaintance关系为图的带权边,边权值为value。在此之外有个Group的额外结构,可以理解为Network的子图。直接采用层次化管理的方式,Network管理PersonGroup,记录查询所有的信息;Group管理Group中的PersonPerson则负责管理邻接的Person和边权value,记录查询自身信息。

1.2.2 性能优化
  • 容器选择

    由于对于每个Person都有独一无二的id,每个Group也有独一无二的id,并且在查询式并不用考虑对象的顺序,所以考虑用Hashmap<id, Objiect>的形式进行存储容器,从而达到把查找的复杂度降到O(1)的目的。

    同时,对于每个Person对象,每个acquaintance关系对应一个value,所以可以采用Hashmap<id, value>的形式存储每个Person对象邻接对象的id和相应权值。这样存储可以避免用两个容器取存储这两个对象。

  • 并查集优化

    本次作业需要降低复杂度的两条方法是MyNetwork里的isCircle(int id1, int id2)queryBlockSum()。根据JML规格解释,isCircle(int id1, int id2)方法用来判断id1id2对应的两个Person是否连通,queryBlockSum()返回Network里连通分支的个数。

    这里的连通查询不需要具体的连通路径,所以可以采用并查集来实现上述的两个方法。建立一个新的并查集类

    JointSearch将其作为MyNetwork类的属性。

    并查集的实现如下,进行了路径压缩和按秩合并:

    private int blockSum;								//连通分支数目
    private final HashMap<Integer, Integer> parent;		//记录根节点的容器
    private final HashMap<Integer, Integer> rank;		//记录秩的容器
    
    /*添加Person对象*/
    public void addPerson(Person person);
    /*查找根节点*/
    public int find(int id);
    /*按秩合并*/
    public void merge(int id1, int id2);
    /*返回查询连通结果*/
    public boolean isCircle(int id1, int id2);
    /*返回连通分支数*/
    public int queryBlockSum();
    

    当加入新的Person对象,由于这个Person对象没有和其他Person对象建立关系(没有连接),所以多了一个新的连通分量,blockSum++;当添加新的关系时,如果两个对象处在不同的连通分支上,则进行合并,blockSum--;按秩合并可以避免树的高度过高,导致在一定数据下查找效率过低的情况。

    最后通过判断两个结点的根节点是否相等,判断是否连通,使得isCircle(int id1, int id2)方法复杂度降为O(log(2)(n)),利用维护的量blockSum使得queryBlockSum()方法复杂度降为O(1)。

2. Homework 10

2.1 UML类图

2.2 分析

2.2.1 架构设计

​ 本次作业在第九次作业的基础上加入了新的Message类,并且增加了NetworkPerson的一些功能,同时在Network增加了查询Group属性的方法。整体架构和第九次作业没有什么区别,增添了PersonNetworkMessage对象的存储和管理。笔者额外建立了Edge类用来存储Network的边。

2.2.2 性能优化
  • 容器选择

    和第九次作业的选择一样,通过Hashmap<id, Objiect>的形式存储可以快速实现查询,不过对于Person对象管理下的Message,由于Message的存储有顺序关系,并且需要在Message的队列头添加元素,选择用LinkedList<Message>进行存储。

  • 查询方法

    本次作业在Network增加了查询Group属性的方法,包括queryGroupAgeVar(int id)queryGroupValueSum(int id)

    如果每次查询都通过遍历来计算的话,复杂度就会变成O(n)或者O(n^2),所以笔者通过在Group中维护三个变量valueSum(2倍权值总和),ageSum(年龄总和),ageSquareSum(年龄平方的总和)。

    维护时需要注意:

    • valueSum:往Group加(减)Person时,将Group中和其邻接的Person的之间边权值的2倍从valueSum加入(减去);添加关系(value)时,如果添加关系的两个对象都在Group中,需要valueSum += 2 * value
    • ageSum:往Group加(减)Person时,需要将其年龄从ageSum加(减)。
    • ageSquareSum:往Group加(减)Person时,需要将其年龄的平方从ageSum加(减)。

    $$
    ageMean = (∑age(i)) / n = ageSum / n
    $$

    $$
    ageVar = ∑ (age(i) - ageMean)^2 = (ageSquareSum + n * ageMean * ageMean - 2 * ageMean * ageSum)/n
    $$

    使得上述变量的查询复杂度降为O(1);

  • Kruskal优化

    根据阅读queryLeastConnection(int id)JML规格(读了很久),发现该方法就是实现一颗最小生成树,由于在第九次作业中实现了并查集,所以这里采用Kruskal算法:

    private final ArrayList<Edge> edges;				//存储连通图的所有边
    private final HashMap<Integer, Integer> parent;		//记录根节点的容器
    private final HashMap<Integer, Integer> rank;		//记录秩的容器
    
    /*在Network中遍历,添加属于该连通分量的边*/
    public void addEdge(Edge edge);
    /*在Network中遍历,添加属于该连通分量的Person对象*/
    public void addPerson(Person person);
    /*查找根节点*/
    public int find(int id);
    /*按秩合并*/
    public void merge(int id1, int id2);
    /*返回查询连通结果*/
    public boolean isCircle(int id1, int id2);
    /*返回最小生成树的边权和*/
    public int leastConnection();
    

    其中,在求最小生成树的边权和时,需要将权值从小到大排序,我重写了Edge类的compareTo()方法,调用Collections.sort()进行排序。

3. Homework 11

3.1 UML类图

3.2 分析

3.2.1 架构设计

​ 本次作业在第十次作业的基础上增加了多种Message,即新增了NoticeMessageRedenvelopMessageEmojiMessage三个Message的子类。在整体架构上没有改变太多。

3.2.2 性能优化
  • 容器选择

    本次作业在第十次作业的基础上添加了EmojiHeat这一概念,其和emojiId是一一对应的,所有采用Hashmap<emojiId, Times>的方法进行存储。

  • Dijkstra算法优化

    根据sendIndirectMessage(int id)JML规格,需要实现一个求最短路径的方法,笔者采用Dijkstra算法并进行了堆优化:

    private HashMap<Integer, Person> people;		//Network中的所有Person对象
    private final HashMap<Integer, Boolean> vis;	//判断结点是否已经被连通
    private final HashMap<Integer, Integer> dis;	//最短路径存储
    private final PriorityQueue<Node> priority;		//优先队列
    private HashMap<Integer, ArrayList<Edge>> edgeTable;//所有的边,Key值为PersonId,value为以其为顶点的所有边
    private static final int INF = 0x3f3f3f3f;
    /*初始化*/
    public void initial(HashMap<Integer, Person> people,
                       int id, JointSearch jointSearch,
                       HashMap<Integer, ArrayList<Edge>> edgeTable);
    /*计算最短路*/
    public int leastValueSum(int id1, int id2);
    

三、性能分析和测试

1. 性能分析

​ 在前两次作业中,笔者对算法的优化都花了不少心思,所以在强测和互测中都没有问题,但是在第三次作业中出现了由于性能问题被卡了一个点的情况,下面进行分析和修复情况。

    public int leastValueSum(int id1, int id2) {
        priority.add(new Node(id1, 0));
        dis.put(id1, 0);
        while (!priority.isEmpty()) {
            Node node = priority.poll();
            int id = node.getSide();
            if (vis.get(id)) {
                continue;
            }
            vis.put(id, true);
            if (vis.get(id2)) {
                return dis.get(id2);
            }
       !!!ArrayList<Edge> temp = edgeTable.get(id);
            for (Edge e : temp) {
                int q;
                if (e.getEndId() != id) {
                    q = e.getEndId();
                } else {
                    q = e.getStartId();
                }
                if (dis.get(q) > node.getValue() + e.getValue()) {
                    dis.put(q, e.getValue() + node.getValue());
                    priority.add(new Node(q, dis.get(q)));
                }
            }!!!
        }
        return dis.get(id2);
    }

​ 上面的感叹号处,笔者在修复前是对Person进行遍历,导致进行的堆优化没有用到,复杂度还是O(n^2),将这里改为对边进行遍历,就用到了堆优化,复杂度降为O((m+n)logn);

2. 测试

​ 本单元作业采用黑盒测试和白盒测试两种测试方法,主要方式是采用和同学对拍的方式。

​ 白盒测试:和同学进行代码逻辑的对拍,不考虑容器和具体实现方式,只对拍逻辑结构和所有的逻辑路径,保证理解的JML规格没有偏差;

​ 黑盒测试:通过自动评测机进行对拍。通过自动数据生成器生成随机数据,将数据分别给同学的代码和自己的代码测试,然后对结果进行对拍,检验代码的正确性。

​ 经过上诉两部分的测试后,代码的逻辑已经几乎没有什么问题了,然后进行强数据构造进行性能测试和边缘数据。比如第十次作业queryGroupAgeVar(int id)queryGroupValueSum(int id)的复杂度,第十一次作业最短路算法的优化等,可以通过不断调用的方式,让高复杂度的方法不断占用CPU;然后就是一些敏感的细节数据,比如第十次作业Group成员的1111上限,很多同学没有注意,可以构造超过1111人进入Group的数据。

四、Network扩展

要求:假设出现了几种不同的Person

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

对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格。

Advertiser、Producer和Customer都继承Person类,新增类advertisement继承Message类。

Producer类有一个属性products,用来存储生产产品信息,不同的产品用id区分。

生产产品

/*@ 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也是产生了新的经验,需要在代码规格和JML规格之外加上自然语言规格,即用自然语言描述JML规格辅助从JML到java代码的转换。

posted @ 2022-06-05 11:31  薛定谔的猫SC  阅读(11)  评论(1编辑  收藏  举报