BUAA OO 2022 第三单元总结

一、结构分析

(一)第一次作业

1、作业要求

本次作业要求为根据GroupNetworkPerson三个接口内部使用JML语言描述的方法,实现相应的三个类,模拟一个社交网络中的群体、个体及其关系;实现六个抽象异常类,要求具有计数功能。

2、模型架构

首先构建三个基本类MyGroup,MyNetwork,MyPerson。MyGroup与MyPerson的基本方法较为简单,按照JML语言描述即可完成,不需要新建额外的类。

MyNetwork中包含大量的异常抛出,故先构建异常类,为方便计数,构造Allnum类,类内对不同id均进行不同异常的计数。每当抛出异常时,相应的id异常与总异常计数均加一,并再次使用该计数实现异常抛出。Allnum内部分计数代码如下:

   public void addEpe(int id) {
       int i;
       for (i = 0; i < epes.size(); i++) {
           if (epes.get(i) == id) {
               epeNums.set(i, epeNums.get(i) + 1);
               break;
           }
       }
       if (i == epes.size()) {
           epes.add(id);
           epeNums.add(1);
       }
   }
  
public void addEpe() {    epeNum++;    }

MyNetwork中存在两个重点方法,均需要将MyPerson抽象为节点图进行思考。一个为isCircle方法,判断两节点之间是否存在路径;另一个为queryPeopleSum方法,求出分支数量。

这两个方法其实时想通的,两个节点在同一个分支中当且仅当两个节点之间存在路径。为实现“分支”这一概念,构建了Friends类,每个MyPerson初始时均包含一个Friends,Friends内只有自身一个节点,每当add relation时,将两个MyPerson的Friends进行并集操作,并利用java浅复制的特性使两个MyPerson包含同一个Friends。据此,isCircle只用判断两个MyPerson节点是否在同一个Friends内部即可;queryPeopleSum的结果可以用整数变量block来记录,每当add person时,分支数加一,即block加一,每当add relation且两者不满足isCircle时,两个分支合并,block减一。相关并集代码如下:

   public void makeFriend(Person person, int value) {
        if (((MyPerson)person).getFriends() != friends) {
            for (Person temp : ((MyPerson)person).getFriends().getFriends()) {
                friends.add(temp);
            }
            ArrayList<Person> old = ((MyPerson)person).getFriends().getFriends();
            for (Person temp : old) {
                ((MyPerson)temp).setFriends(friends);
            }
        }
    }

3、测试样例构造

采用大量随机生成并与同学进行对拍的方法。

先生成大量随机add person与add relation,id限制在5000以内,之后各指令随机生成。

指令可以出现重复以测试异常;针对性增加qbs/qci的数量,全面测试性能问题;增加一定数量add group检查group人数1111的限制满足情况。

运行时间与cpu时间采用第六次作业讨论区中给出的方法(http://oo.buaa.edu.cn/assignment/335/discussion/1172)进行测试。

4、bug分析

本次作业我自己的代码出现了两处bug。一处为isCircle方法未构建Friends类,而是采用递归遍历的方法;另一处为queryPeopleSum方法未使用变量进行记录,而是遍历各节点并利用isCircle方法进行判断计数。两处错误均因为暴力遍历导致cpu超时。

(二)第二次作业

1、作业要求

在第九次作业的基础上,增加收发私聊与群发消息、查询最小关系(最小生成树)等功能,并为新增功能实现新的异常类。

2、模型架构

本次作业新增异常参考之前作业完成即可,新增MyMessage模块内方法较为简单,故不做讨论。

重点方法为MyNetwork类内的queryLeastConnection方法求指定MyPerson所在分支的最小生成树的总权重,在Friends内增加路径集合与相应节点对集合,使用flag标志位记录是否因成员/关系变化而需要生成新的最小生成树。在Friends使用subFriends与sunValues数组记录最小生成树,为每个MyPerson分配新的Friends subFriends来表示最小生成树内的所在分支。相应的并集操作改进与克鲁斯卡尔生成方法的代码如下:

    public void makeFriend(Person person, int value) {
        if (((MyPerson)person).getFriends() != friends) {
            for (int i = 0; i < ((MyPerson)person).getFriends().getLineValues().size(); i++) {
                friends.add(((MyPerson) person).getFriends().getLines().get(2 * i),
                        ((MyPerson) person).getFriends().getLines().get(2 * i + 1),
                        ((MyPerson) person).getFriends().getLineValues().get(i));
            }
            for (Person temp : ((MyPerson)person).getFriends().getFriends()) {
                friends.add(temp);
            }
            ArrayList<Person> old = ((MyPerson)person).getFriends().getFriends();
            for (Person temp : old) {
                ((MyPerson)temp).setFriends(friends);
            }
        }
        friends.add(this, person, value);
    }
   public void makeSub() {
        if (subFlag == 0) {
            return;
        }
        subFriends = new ArrayList<>();
        subValues = new ArrayList<>();
        for (Person person : friends) {
            Friends temp = new Friends();
            temp.add(person);
            ((MyPerson)person).setSubFriends(temp);
        }
        for (int i = 0; i < lineValues.size(); i++) {
            if (!subFriends.contains(lines.get(2 * i)) ||
                    !subFriends.contains(lines.get(2 * i + 1)) ||
                    ((MyPerson)lines.get(2 * i)).getSubFriends() !=
                            ((MyPerson)lines.get(2 * i + 1)).getSubFriends()) {
                subValues.add(lineValues.get(i));
                if (!subFriends.contains(lines.get(2 * i + 1))) {
                    subFriends.add(lines.get(2 * i + 1));
                }
                if (!subFriends.contains(lines.get(2 * i))) {
                    subFriends.add(lines.get(2 * i));
                }
                for (Person temp : ((MyPerson)lines.get(2 * i + 1)).getSubFriends().getFriends()) {
                    ((MyPerson)lines.get(2 * i)).getSubFriends().add(temp);
                    ((MyPerson)temp).setSubFriends(((MyPerson)lines.
                            get(2 * i)).getSubFriends());
                }
            }
        }
        subFlag = 0;
    }

3、测试样例构造

在上一次作业的自动生成样例的随机生成指令中,完善新增功能,增加add relation与add to group数量,尽可能多的使用qlc查询最小生成树与qgvs查询组内权重以保证性能。

4、bug分析

克鲁斯卡尔法最小生成树加入最小边时仅考虑端点是否包含,未考虑端点是否连通,导致最小生成树内部可能出现多个分支。

每次add relation时生成一次最小生成树,导致cpu超时,更改为每次查询最小生成树时检测标志位flag是否需要重新生成最小生成树。

每次query_group_value_sum时均双重遍历一次组内成员导致cpu超时,更改为使用变量记录,每次group人员更新或关系变化时更新变量。

(三)第三次作业

1、作业要求

在第十次作业的基础上,增加红包收发,收发emoji并对emoji的热门程度排序,除去冷门emoji,收发间接消息(堆优化的迪杰斯特拉图)等功能,并为新增功能实现新的异常类。

2、模型架构

本次作业重点在于MyNetwork中的sendIndirectMessage方法,构建迪杰斯特拉图以查询最短路径。

在Friends类内,构建二维数组迪杰斯特拉图minMao,每次增加节点时扩展二维数组,每次仅增加关系时,查询是否需要更新矩阵元素。

每次生成迪杰斯特拉图时记录起点,同时使用标志位记录节点与关系是否发生变化,若未发生变化且起点与上一次的起点形同,则不必重新构建。相关构建代码如下:

    public int makeMin(Person p1, Person p2) { //制作迪杰斯特拉图
        if (minFlag == 1 ||
                !((preLeft == p1 && preRight == p2) || (preLeft == p2 && preRight == p1))) {
            preLeft = p1;
            preRight = p2;
            ArrayList<Integer> used = new ArrayList<>();//0为未使用,1为已标记,2为在优先队列中
            for (Person ignored : friends) {
                used.add(0);
            }
            prio = new ArrayList<>();//优先队列,存储序号
            prio.add(friends.indexOf(p1));
            used.set(friends.indexOf(p1), 2);
            while (prio.size() > 0) {
                int target = minPrio(p1);
                used.set(prio.get(target), 1);
                for (int i = 0; i < friends.size(); i++) {
                    if (friends.get(i).isLinked(friends.get(prio.get(target)))) {
                        if (used.get(i) == 0) {
                            prio.add(i);
                            used.set(i, 2);
                        }
                        if (used.get(i) != 1) {
                            if (getMap(friends.indexOf(p1), prio.get(target)) +
                                    getMap(prio.get(target), i) < getMap(friends.indexOf(p1), i)) {
                                setMap(friends.indexOf(p1), i, getMap(friends.indexOf(p1),
                                        prio.get(target)) + getMap(prio.get(target), i));
                                setMap(i, friends.indexOf(p1), getMap(friends.indexOf(p1),
                                        prio.get(target)) + getMap(prio.get(target), i));
                            }
                        }
                    }
                }
                prio.remove(target);
            }
        }
        minFlag = 0;
        return getMap(p1, p2);
    }

3、测试样例构造

在上一次作业的自动生成样例的随机生成指令中,完善新增功能,尽可能多的使用sim查询迪杰斯特拉图以保证性能。

4、bug分析

在扩展迪杰斯特拉图时,addMap(Person person)函数的前置条件为friends的数量仅增加了1,但在调用函数时先将所有人加入friends再使用addMap(Person person),造成前置条件不满足,数组越界产生断点。

构建迪杰斯特拉矩阵的策略错误,仅构建了起点为friends[0]矩阵。更改为每次生成迪杰斯特拉图时记录起点,同时使用标志位记录节点与关系是否发生变化,若未发生变化且起点与上一次的起点形同,则不必重新构建。

(四)UML类图与整体架构

UML类图

 

 

 

 本单元作业自己实现的类的类图如上图,由于自定义异常类的重复性较高,因此类图中只出现了MyPersonIdNotFoundExceotion与MyEqualPersonIdException作为例子。

自添加类

非题目要求的类包括Friends类,Allnum类,Tools类。

Friends类

Friends类用于构建分支图,一个分支内的节点共用一个Friends类,并以分支图为基础在该类内生成迪杰斯特拉图与克鲁斯卡尔图,其中friends为节点集合,lineValues为边的权重集合,lines为边节点的集合(一个lineValue元素对应两个line元素),subFriends表示克鲁斯卡尔图内的节点结合,subValues表示克鲁斯卡尔图内的边权重集合,subFlag为克鲁斯卡尔图是否需要重新生成的标志位(是否加入了新的边),minFlag为迪杰斯特拉图是否需要重新生成的标志位(是否加入了新的节点或新的权重小于已有迪杰斯特拉图对应元素的边),preLeft为上一次迪杰斯特拉图的起点,perRight为上一次迪杰斯特拉图的终点,minMap为迪杰斯特拉图,prio为生成迪杰斯特拉图所用的优先队列。标志位的使用,有效避免了重复消耗cou资源不断生成迪杰斯特拉图或克鲁斯卡尔图造成的资源浪费。

Allnum类

Allnum类用于记录各类异常出现的次数与各个id出现各类异常的次数,epeNum存储epe异常出现的总次数,epes存储出现epe异常的id,epeNums存储epes中各id出现epe异常的次数。

Tools类

Tools类用于专门生成即将抛出的异常。

MyPerson类

MyPerson类中,使用Friends类存储所在分支,便于isCircle以及相关指令的查询。

MyGroup类

MyGroup类中,使用sumValue来记录组内总权重,避免重复遍历相加造成的cou资源浪费。

MyNetWork类

MyNetWork类中,block记录分支数量,避免遍历节点计算分支数量造成的资源浪费;迪杰斯特拉图与克鲁斯卡尔图等复杂图运算均在Friends类内实现。

容器维护策略

本单元作业中使用到的数组类容器均为ArrayList,查询时进行遍历查询;

对于对顺序有对应关系的数组,例如Friends中的lines与lineValues,要求每一个lineValues[i]对应lines[2i]与lines[2i+1],由于不涉及删除操作,因此只需确保添加数组时二者同时添加即可,同时为方便计算,添加时按照lineValues的大小顺序插入到相应位置;

克鲁斯卡尔算法通过每个person的“第二分支Friends”、Friends中的节点数组subFriends与权重数组subValues进行记录,以lines与lineValues为原始图,依次选取最小权重的lineValue,若相应的两个lines节点不存在于subFriends或不存在于同一个第二分支Friends,则将对应lineValue复制入subValues,并使subFriends包含对应的两个lines节点;若需要重新生成则将记录清空。

迪杰斯特拉图有二维ArrayList数组组成,每次新添加节点时,已有的每行ArrayList末尾添加最大值(2000),添加一行ArrayList与已有ArrayList长度对齐,将存在较小直接关系的位置元素进行更新,由于不涉及删除节点或关系操作,因此每次重新生成迪杰斯特拉图时不必初始化二维数组,直接在原数组上进行计算即可。

二、单元拓展

1、题目要求

假设出现了几种不同的Person

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

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

2、拓展方式

从功能性的角度来讲,Advertiser、Producer、Customer均可以作为Person的子类,Advertiser添加发送广告、接受购买消息的方法,Producer添加生产产品、发货的方法,Customer添加发送购买消息方法;产品广告与购买消息均作为Message的子类。

选择发送广告、生产产品、发货三个方法撰写JML规格。

发送广告

    /*@ public normal_behavior
      @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement);
      @ assignable messages, people[*].messages;
      @ ensures messages.length == \old(messages.length) - 1;
      @ ensures (\forall int i; 0 <= i && i < messages.length;
      @          (\exists int j; 0 <= j && j < \old(messages.length); messages[i] == (\old(messages[j]))));
    @ ensures !(\exists int i; 0 <= i && i < messages.length; getMessage(id) == 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().length); @ people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) && @    people[i].getMessages().get(0) == (\old(getMessage(id))) && @    people[i].getMessages().length == \old(people[i].getMessages().length) + 1); @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); @ signals (AdvertisementTypeException e) !(getMessage(id) instanceof Advertisement); @
*/ public void sendAdvertisement(int id) throws MessageIdNotFoundException, AdvertisementTypeException;

生产产品(producer包含现存产品与已售出产品)

   /*@ public normal_behavior
      @ requires !containsAllProduct(productId);
    @ assignable things;     @ ensures things.length == \old(things.length) + 1;     @ ensures (\forall int i; 0 <= i && i < \old(things.length);     @      (\exists int j; 0 <= j && j < things.length; things[j] == \old(things[i])));     @ ensures
(\exists int i; 0 <= i && i < things.length; things[i] == getProduct(productId));
    @ alse
    @ public exceptional_behavior
    @ signabls (ProductSameException) containsAllProduct(productId);
    @*/
   public void makeProduct(int productId) throws ProductSameException;

发货

   /*@ public normal_behavior
      @ requires containsProduct(productId);
    @ assignable things, outThings, money, person.money, person.things; @ ensures person.money == \old(person.money) - getProduct(productId).getValue;
    @ ensures money == \old(money) - getProduct(productId).getValue;
    @ ensures outThings.length == \old(outThings.length) + 1;
   
@ ensures (\forall int i; 0 <= i && i < \old(outThings.length);
   
@      (\exists int j; 0 <= j && j < outThings.length;
   
@       outThings[j] == \old(outThings[i])));
    @ ensures (\exists int i; 0 <= i && i < outThings.length; outThings[i] == getProduct(productId));
@ ensures things.length == \old(things.length) - 1;
    @ ensures (\forall int i; 0 <= i && i < things.length;
    @      (\exists int j; 0 <= j && j < \old(things.length); things[i] == \old(things[j])));
    @ ensures !(\exists int i; 0 <= i && i < things.length; things[i] == getProduct(productId));
    @ ensures person.things.length == \old(person.things.length) + 1;
   
@ ensures (\forall int i; 0 <= i && i < \old(person.things.length);
   
@      (\exists int j; 0 <= j && j < person.things.length; person.things[j] == \old(person.things[i])));
    @ ensures (\exists int i; 0 <= i && i < person.things.length; person.things[i] == getProduct(productId));
    @ alse
    @ public exceptional_behavior
    @ signabls (ProductIdNotFoundException) !containsProduct(productId);
    @*/
   public void sendProduct(Person person, int productId) throws ProductIdNotFoundException;

三、学习体会

本单元采用了大量的自定义异常,让我对java的异常抛出的定于与原理有了更加深入的了解;本单元作为JML单元,也让我能够对JML语言有了一个较为熟练的理解与运用,让我体会到方法能够通过严谨的定义来避免许多不必要的bug。

为实现部分较为复杂的功能,必要时可以单独创建一个类专门用来储存相关数据并进行操作。本单元作业中在第一次作业创建的Friends类可以看到在三次作业的关键部分都起到了重要的容器作用,这一实践证明了适当地添加类对代码的后续开发迭代能够起到重要作用。

对于需要遍历或递归实现的操作,要多多思考能否用动态规划的思路,将可能需要的结果提前以变量或数组的形式存储,在查询时只需要直接取出即可,可以有效避免重复遍历导致的超时问题。

posted @ 2022-06-04 22:20  璇璃  阅读(30)  评论(2编辑  收藏  举报