BUAA-OO2022-UNIT3总结
1. JML基础总结
-
变量规格
静态规格变量:
//@public static model non_null int []elements
实例规格变量:
//@public instance model non_null int []elements
-
方法规格
-
normal_behavior
- 前置条件(requires):行为执行的前提条件,不满足则不做动作
- 后置条件(ensures):行为执行的结果。用\result表示结果。
- 副作用(assignable):行为执行期间对对象状态的改变。(可以改变哪些对象的状态)
-
exceptional_behavior
- 前置条件(requires):通常与正常行为的前置条件互补
- 后置条件(ensures):通常没有(也许只是我还没见到过吧,捂脸/.jpg)
- signals子句:最常用,通过signals子句来抛出异常。
-
-
一些常用“变量”与表示:
含义 JML 全称量词 \forall 存在量词 \exists 充分 ==> 必要 <== 充要 <==> 除此之外,JML还有很多方便的符号:
含义 JML 对给定范围内满足条件的表达式和 \sum 对给定范围内满足条件的表达式的积 \product 对给定范围内表达式求最大(最小) \max(\min) 求给定范围内满足条件的个数 \num_of ……
2. 测试数据
对于JML测试而言,首要任务就是仔细阅读规格,在源头上避免方法实现错误。
通常的测试可以采用Junit,其测试方法类似于上学期计组的testbranch,进行assert操作,可以初步验证方法的正确性。
本次没有自己写整体的评测机,主要是手造数据,在针对某些特定指令时利用利用程序随机生成输入。
当考虑测试数据时,主要从以下方面考虑:
- normal_behavior和exception_behavior都要覆盖
- 随机生成满足前置条件的数据,用函数测试
- 手动构造容易超时的数据,如针对图最短路径查找的操作。
3. 架构设计
这三次作业架构都差不多,建立一个社交网络,其中有Message,Person,Group三种“成员”,在Network类中实现相互之间的交互和各种查询。
4. 图模型构建和维护
本次新建了一个Block类进行最小生成树、图、并查集的维护与简化。
Block中保存一个连通块信息:
- Hashset——linkedPeople:连通块中所有人
- valueSum:最小生成树的value总和
- mst:最小生成树的邻接表,同时保存value。
左右在这个连通块内的人都有一个属性指向相同的Block
简化的并查集
第9次作业时助教介绍了并查集,可以对相互认识的人之间进行低复杂度的处理。由于在实际查询时,有不涉及并查集具体树结构的查询操作,可以直接判断两者是否指向同一个联通块即可——linkedBlock属性是否指向同一个Block。
连通块的合并:判断两个结点A、B是否处于同一个连通块,只需要判断A的Map中是否含有B。增加关系事实上等价于连通块的合并,可以先判断两个结点是否位于同一个连通块中(即调用isCircle方法),如果不在的话,就修改数目较少的连通块中结点的引用。
最小生成树维护与简化查询
-
最小生成树维护
在每次建立联系时考虑连通块更新。
-
!isCircle——不在同一个连通块
这种情况可以考虑联通块的合并,然后直接将原来的两个最小生成树加上一条新边
-
isCircle——原来就在一个连通块
这种情况不需要连通块合并,但是需要更新最小生成树。添加了这条边后原来的最小生成树一定会多一条回路,因此直接在原来的最小生成树中dfs遍历建立联系两个结点之间的路径(一定是唯一的),然后寻找这条回路中权值最大的边,从而进行最小生成树的简单更新。
-
-
简化计数。
在每一次更新最小生成树时会保留各种信息,之后直接取出即可。
图的维护与最短路径
在Block中记录了连通块中的所有Person,而在每个Person中保存了与其直接相连的结点以及对应边的权重。因此通过遍历Block中所有Person可以对图进行遍历。
由于最短路径取决于其实Person,因此在每个Person中维护一个保存最短路径的邻接表dst,每次查询最短路径时先查dst是否已经计算好,若没有则重新计算并把结果保存在dst中;当该Person所在连通块中发生更新是需要清空dst,下一次查询时重新计算最短路径。
这样可以减少一部分每次查询都重新计算最短路径的时间花销。
5. 性能问题和修复
部分性能优化已在“图模型构建和维护”中提到,其余的部分:
- 第一次作业很匆忙,queryGroupAgeVar的时候采用的是遍历方法,结果互测时超时严重。之后改成在group里建立维护一个ageSum(年龄和)和ageSumSquare(年龄平方和),之后在查询时就不用再遍历。
- 最开始算最短路径时是有的是直接遍历,怀疑会超时,之后进行了优化,改成了小顶堆优化的Dijkstra,时间复杂度为O(|E|log|V|)。
6. Network扩展
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
- Network:支持市场营销,并能查询某种商品的销售额和销售路径等
基本架构
-
建立一个Message类用来表示相互之间传递的消息,用type属性来表示消息类别——广告或购买。
-
Advertiser、Producer、Customer都继承Person,作为其子类
- Advertiser:与Producer建立关联,记录其发送的广告对应的产品
- Producer:与Advertiser建立关联,记录发送其广告的对应广告者
- Customer:与Advertiser建立关联,记录关注的广告者
-
Network中维护一系列当前的Advertiser、Producer、Customer、Message的数据结构
-
其余部分于本单元的社交网络可以以相似的模式构建,比如如下的基本操作:
public /*@ pure @*/ boolean containsAdvertiser(int id); public /*@ pure @*/ boolean containsProducer(int id); public /*@ pure @*/ boolean containsCustomer(int id); public /*@ pure @*/ boolean containsMessage(int id); public /*@ pure @*/ Person getPerson(int id); public void addPerson(/*@ non_null @*/Person person, int type) throws EqualPersonIdException; //type表示是哪一种人 //添加消费者关注的广告商 public void addAttention(int id1, int id2, int value) throws PersonIdNotFoundException, EqualRelationException; //添加广告商要投放广告的生产商 public void addCorporation(int id1, int id2, int value) throws PersonIdNotFoundException, EqualRelationException; //查询id制造商制造的商品的销售额 public /*@ pure @*/ int queryPrice(int id) throws ProducerIdNotFoundException; //查询id商品的销售路径(代理广告商) public /*@ pure @*/ Advertiser queryAgent(int id) throws ProducerIdNotFoundException;
核心功能及接口方法
-
添加广告
把message加入到消息队列中
/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ message.getType() == 2 && (message.getPerson1() instanceof Advertiser); @ assignable messages; @ ensures messages.length == \old(messages.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(messages.length); @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)); @ also @ public exceptional_behavior @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)); @ signals (noAdvertiserException e) !(\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)) && !(messages[i].getPerson1() @ instanceof Advertiser); @*/ public void addAdvertisement(Message message) throws EqualMessageIdException, noAdvertiserException;
-
发送广告
把id是id的Message发送到对应的人
/*@ public normal_behavior @ requires containsMessage(id) && getMessage(id).getType() == 2 && @ getMessage(id).getPerson1() instancedof Advertiser; @ assignable 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 < \old(getMessage(id).getPerson1().acquaintance[i]); @ \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(i+1) == \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(i))); @ ensures \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(0).equals(\old(getMessage(id))); @ ensures \old(getMessage(id)).getPerson1().acquaintance[i].getMessages().size() == \old(getMessage(id).getPerson1().acquaintance[i].getMessages().size()) + 1; @ also @ public exceptional_behavior @ signals (noAdvertisementException e) getMessage(id).getType() != 2; @ signals (MessageIdNotFoundException e) !containsMessage(id); @ signals (noAdvertiserException e) containsMessage(id) && getMessage(id).getType() == 2 && @ !(getMessage(id).getPerson1() instanceof Advertiser); @*/ public void sendAdvertisement(int id) throws MessageIdNotFoundException, noAdvertisementException, PersonIdNotFoundException, noAdvertiserException;
-
添加购买信息
类似添加广告信息,把购买信息加入消息队列中
/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && @ message.getType() == 3 && (message.getPerson1() instanceof Advertiser) && (message.getPerson2() @ instanceof Customer) && (message.getPerson3() instanceof Producer); @ assignable messages; @ ensures messages.length == \old(messages.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(messages.length); @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)); @ also @ public exceptional_behavior @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)); @ signals (noAdvertiserException e) !(\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)) && !(messages[i].getPerson1() @ instanceof Advertiser); @ signals (noCustomerException e) !(\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)) && !(messages[i].getPerson1() @ instanceof noCustomerException); @ signals (noProducerException e) !(\exists int i; 0 <= i && i < messages.length; @ messages[i].equals(message)) && !(messages[i].getPerson() @ instanceof noProducerException); @*/ public void addPurchaseMessage(Message message) throws EqualMessageIdException, noAdvertiserException, noCustomerException, noProducerException; //添加购买消息
-
发送购买信息
类似发送广告信息。
7. 本单元学习体会
本单元从难度上比之前小了不少,主要是根据官方包提供的JML来进行功能的具体实现。相较于之前的单元,本单元不用自己涉及架构,基本架构已经由官方包和JML给出;同时,测试起来也比之前方便,可以根据JML进行针对性测试,也不像多线程输出不可控。
但是本单元还是有许多值得花功夫的点。
- JML本身的熟悉与在读JML的时候理解“标准化”的思想,其实以前我们写代码是常出错的不是“实现”过程,而是“设计”过程,漏掉了某个条件,或者可能性没考虑全。那么这次阅读JML的时候则可以体会到一些设计的模式,一些常用的思考问题的角度,有助于我们以后自己的设计。
- 图、树算法的复习,很久没有用数据结构了,这次作业中也是对这方面的一个复习。
- 规范化测试数据生成的经验。以前的测试要么是瞪眼法,偶尔写了评测机也是随机生成测试样例。而这一单元作业结合Junit尝试去针对性地构造测试数据,对某个函数进行单独测试,而非整个流程拉通测试,这样效率明显更高。
本单元结束后个人还有相对薄弱的点,就是写JML,虽然读别人写好的很轻松,但自己写的时候仍然会出现漏掉边界情况,或者对于一个功能不知道怎样合理地用JML的语言表达出来,之后有时间可以加强。