OO第三单元总结
OO第三单元总结
一、利用JML规格准备测试数据
1.正确性测试
利用JML规格中的also连接词来快速识别normal_behavior中的不同前置条件以及exceptional_behavior中的不同异常触发条件,使得数据可以尽量覆盖上述的所有情况。而对于每种情况,可以人工构造一些边界条件(如size==0或1,输入的两个参数相同等)以加强对正确性的测试效果。
2.复杂度测试
主要针对时间复杂度较高的指令进行测试,利用程序输出满足数据限制的含有大量高时间复杂度指令的数据以检测项目是否可能在数据限制内触发TLE。
一个利用Python程序输出大量高时间复杂度指令数据的例子:
for i in range(2440):
print("ap " + str(i + 1) + " p" + str(i + 1) + " 20")
for i in range(2419):
print("ar " + str(i + 1) + " " + str(i + 2) + " 1000")
for i in range(50):
print("qci " + str(i + 1) + " " + str(i + 2))
print("qci " + str(2420 - i) + " " + str(2419 - i))
for i in range(20):
print("ar " + str(i + 2420) + " " + str(i + 2421) + " 1000")
print("qlc " + str(i * 128 + 1))
二、架构设计
1.图模型的构建与维护
本单元作业中,图模型的构建和维护工作主要都在Network类中:
Network类分别用四个HashMap存储people、groups、messages、emojis,其中键值(key)是对象所拥有的ID,value是对象本身。而people则是最关键的图节点,MyPerson类中用ArrayList存储图中的边和边的权值,图的特征基本完整,message是对图的节点和边进行操作的工具之一。
具体地,Network类中属性和方法如下图所示:
MyPerson、MyGroup、Mymessage等类负责存储自身的属性、提供可供Network调用的方法(如setter、getter、对属性有关参数的计算、并查集的创建与合并等)、此类的构造方法等。
2.异常计数
由于JML对异常的计数需求,项目增添Counter类用以记录各类异常发生的次数及以及对应ID触发该类异常的次数。
具体地,Counter类利用count方法实现计数功能,输入type值为0时才对异常总数进行一次计数,否则仅对该ID所对应异常数进行计数,这是对双ID异常的特殊处理方式。(实现如下图所示)
private int num;
private final HashMap<Integer, Integer> idNums;
public Counter() {
num = 0;
idNums = new HashMap<>();
}
public void count(int id, int type) {
if (type == 0) {
num++;
}
if (!idNums.containsKey(id)) {
idNums.put(id, 0);
}
Integer prev = idNums.get(id);
idNums.put(id, prev + 1);
}
在各异常类中,利用静态量COUNTER(Counter类的实例化)记录该类异常发生的总次数。
单ID异常的实现如下图所示:(以空EmojiId异常为例)
private static final Counter COUNTER = new Counter();
private final int id;
public MyEmojiIdNotFoundException(int id) {
COUNTER.count(id, 0);
this.id = id;
}
双ID异常的实现如下图所示:(以相同关系异常为例)
private static final Counter COUNTER = new Counter();
private final int id1;
private final int id2;
public MyEqualRelationException(int id1, int id2) {
COUNTER.count(id1, 0);
if (id1 != id2) {
COUNTER.count(id2, 1);
}
this.id1 = id1;
this.id2 = id2;
}
三、性能分析
1.发现与分析问题
① 第一次作业中,对判断两节点是否可达的函数(isCircle)编写采用了深度优先遍历算法,导致时间复杂度较高,在强测中出现大量TLE。这也与第一次作业未做性能测试有较大关系。
② 第二次作业中,互测中被发现计算图的分支数的函数(queryBlockSum)产生的TLE,这是由于编写函数时未对JML的真实含义进行思考与发掘,仅实现JML表述所对应的代码,造成时间复杂度为o(n^2)。而第二次作业新增添的计算最小生成树的权值的函数(queryLeastConnection)采用了基于并查集优化的prim算法,性能较好,因此在强测中并未产生TLE的情况。
③ 第三次作业中,强测有一个点产生TLE,这是计算最短路径的函数(queryLeastPath(被sendIndirectMessage调用))产生了TLE。该函数采用了普通的迪杰斯特拉算法,但由于测试数据刻意为TLE而设计,并非是正常情况,因此该算法的性能无法达到要求。
2.解决问题
① 将深度优先遍历算法更改为并查集算法,大大降低了时间复杂度。
② 利用并查集算法,将遍历到的根节点放入map,最后仅统计map中的根节点总数即可,一般情况下的时间复杂度远低于原来的o(n^2)。
③ 试图采用基于堆优化的迪杰斯特拉算法降低复杂度,但在bug修复环节发现修改后仍然无法满足此强测数据点的性能要求,目前仍不知道问题出在何处。
四、项目扩展
1.发送广告
Advertiser发送广告相当于对所有人(除自己)发送一条广告消息(AdvertiseMessage),广告消息的person1是广告商,其属性包含广告的各种有效信息,此处忽略Person其他属性变化的细节
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 2;
@ 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 Person p; people.hasPerson(p) && p != \old(getMessage(id)).getPerson1;
@ p.getPreference(\old(getMeassage(id)).getCommodityId()) == \old(p.getPreference(getMessage(id).getCommodityId())) + (\old(getMessage).getEffect()));
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id) || (containsMessage(id) && getMessage(id).getType != 2);
@ */
public void sendAdvertisement(int id) throws MessageIdNotFoundException;
2.增加偏好
如上述JML所示,消费者收到广告消息后会增加该商品对应的偏好值(preferences 属性的对应元素)
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i <= preferences.length; commodities[i].getId() == id);
@ assignable preferences;
@ ensures (\exists int i; 0 <= i && i <= preferences.length && commodities[i].getId() == id; preferences[i] == \old(preference[i]) + num);
@ ensures (\forall int i; 0 <= i && i <= preferences.length && commodities[i].getId() == id; preferences[i] == \old(preference[i]));
@ also
@ public exceptional_behavior
@ signals (CommodityIdNotFoundException e) (\forall int i; 0 <= i && i <= preferences.length; commodities[i].getId() != id);
@ */
public void addPreference(int id, int num) throws CommodityIdNotFoundException;
3.购买商品
直接通过Advertiser给相应Producer发一个购买消息(PurchaseMessage),购买消息属于第0类消息,person1是广告商,person2是生产商,其属性包含商品信息、顾客信息,此处忽略Person其他属性变化的细节
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id) instanceof PurchaseMessage &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages, getMessage(id).getPerson2().money;
@ assignable getMessage(id).getCustomer().money, getMessage(id).getCustomer().commodities;
@ 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 getMessage(id).getPerson2().getmoney() == \old(getMessage(id).getPerson2().getMoney()) + getMessage(id).getPrice();
@ ensures getMessage(id).getCustomer().getMoney() == \old(getMessage(id).getCustomer().getMoney()) - getMessage(id).getPrice();
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getCustomer().getCommodities().size());
@ \old(getMessage(id)).getCustomer().getCommodities().get(i + 1) == \old(getMessage(id).getCustomer().getCommodities().get(i)));
@ ensures \old(getMessage(id)).getCustomer().getCommodities().get(0).equals(\old(getMessage(id).getCommodity()));
@ ensures \old(getMessage(id)).getCustomer().getCommodities().size() == \old(getMessage(id).getCustomer().getCommodities().size()) + 1;
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (MessageIdNotFoundException e) containsMessage(id) && getMessage(id).getType() != 0;
@ signals (MessageIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id) instanceof PurchaseMessage);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id) instanceof PurchaseMessage &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ */
public void purchase(int id) throws MessageIdNotFoundException, RelationNotFoundException;
五、学习体会
① 通过本单元的学习,我对JML的理解显著加深从,阅读JML的能力明显提高(从无到有),但根据代码或架构编写JML的能力仍然有待加强。
② 经过理论课的学习和作业中对JML到代码的转换过程的思考,我认识到了JML在项目开发中的重要作用,以及规格化对复杂度较高的工程以及团队协作和正确性检验等有着较大帮助。
③ 本单元的作业在帮助我们熟悉JML的同时,也让我们对并查集算法、图论的相关算法有了具体的体会和理解,然而我个人认为评测时利用TLE提高对算法复杂度的要求让我们的精力更多地集中在对算法的研究上,反而会忽视对JML语义的理解,似乎有喧宾夺主之嫌。
④ 总体上,本单元作业还是让我对规格化的层次设计有了更深的了解,提高了工程开发和代码编写能力,收获颇丰。