BUAA_OO_2022 Unit3 总结
自测过程
数据准备
由于本单元的代码完全基于JML,因此数据生成也主要参考了JML规格。在数据生成器中,我通过参数控制各个指令的出现频率和出现顺序,以尽可能覆盖规格中出现的各类情况。
在此基础上,为了更好地覆盖边界情况以及测试程序的抗压能力,数据生成器支持生成完全图、菊花图、长链图等特殊的图论模型,支持自由控制group的个数。
下面附上一些数据生成器的一些参数
int personId = 0, groupId = 0, messageId = 0, emojiId = 0, sendId = 0, currentEmojiId = 0;
int maxag = 25, maxap = 5000, maxqci = 333, maxqlc = 100, maxsim = 1000, maxEmojiId = 10;
int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0, cntsim = 0;
int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception, 5 complete, 6 sim, 7 emoji, 8 notice, 9 super
bool noGroup = false, emojiFull = false, oneGroup = false;
JUnit
JUnit的核心是使用assert检验JML里的后置条件和不变式是否满足。大致思路是在需要测试的方法中,先调用初始化的方法来建立测试所需要的环境,然后调用待测试的方法,并使用assert来检验该方法运行是否正确。
下图是一个范例(by qs同学)
架构分析
图模型的构建
本单元作业背景是社交网络模型,Person可以抽象为图中的点,Relation可以抽象为图中的边,Group可以抽象为点集。除Message相关的方法基本都是围绕着图的建立、更新和查询。
图论算法
部分图的查询方法用到了初等图论算法,qbs和qci用到了并查集、qlc用到了最小生成树、sim用到了单源最短路。
最小生成树算法我采用的是Kruskal,理由是方便复用之前已经实现的并查集算法,具体代码如下
private int kruskal(int id) {
HashMap<Integer, Integer> tmpFather = new HashMap<>();
int cnt = 0;
for (Integer x : people.keySet()) {
if (find(father, id) == find(father, x)) {
cnt++;
tmpFather.put(x, x);
}
}
int ret = 0;
Collections.sort(edges);
--cnt;
for (Edge edge : edges) {
if (!tmpFather.containsKey(edge.getNodeX())
|| !tmpFather.containsKey(edge.getNodeY())) {
continue;
}
if (find(tmpFather, edge.getNodeX()) != find(tmpFather, edge.getNodeY())) {
ret += edge.getValue();
merge(tmpFather, edge.getNodeX(), edge.getNodeY());
--cnt;
if (cnt == 0) {
break;
}
}
}
return ret;
}
单源最短路我采用的是堆优化的Dijkstra算法,理由是由于公测的数据限制,图在极限情况下是稀疏图,边权非负,此时堆优化的Dijkstra算法效果较优。具体代码如下
private int dijkstra(int st, int ed) {
HashMap<Integer, Integer> dis = new HashMap<>();
HashMap<Integer, Boolean> vis = new HashMap<>();
for (Integer personId : people.keySet()) {
dis.put(personId, 0x3f3f3f3f);
vis.put(personId, false);
}
dis.put(st, 0);
PriorityQueue<MyPair> q = new PriorityQueue<>();
q.add(new MyPair(0, st));
while (!q.isEmpty()) {
int x = q.peek().getValue();
q.poll();
if (vis.get(x)) {
if (x == ed) {
break;
}
continue;
}
vis.put(x, true);
HashMap<Person, Integer> acquaintance = ((MyPerson) getPerson(x)).getAcquaintance();
for (Map.Entry<Person, Integer> entry : acquaintance.entrySet()) {
int y = entry.getKey().getId();
int z = entry.getValue();
if (dis.get(y) > dis.get(x) + z) {
dis.put(y, dis.get(x) + z);
q.add(new MyPair(dis.get(y), y));
}
}
}
return dis.get(ed);
}
数据结构思想
group中的一些查询方法若严格按照JML将会达到O(n^2)的复杂度,明显会超时。因此,需要采用数据结构的思想,维护一些中间变量,以保证单次方法的均摊复杂度在O(n)以内。
容器的选择
JML并没有限制我们使用哪种容器,因此需要我们自己根据该容器在JML中的使用特性来做出选择。本单元中查询和更新的频率相当,且基本是随机存取,因此我基本都选择了HashMap来实现单次存取O(log(n))的复杂度。一个例外是维护Person收到的Message用的是ArrayList,因为对它的每次插入和查询操作都集中在链表头部。
性能问题和修复情况
虽然三次作业都写了数据生成器,但是三次作业在写完后课下对拍时都没被拍出bug,公测和互测也没有出bug。(感觉数据生成器白写了)
性能问题上,由于一开始就估计了时空复杂度的范围,采用了合适的算法和数据结构来维护,因此没有出现性能问题。
在互测中,测出的问题基本也都是由于时间复杂度过大导致的TLE。
Network扩展
题目假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买(所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息)
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
显然,Advertiser、Producer和Customer均应继承自Person类;Producer会增加一个属性来维护其生产的产品的id,Advertiser会增加一个属性来维护其向外发送广告的产品的id,Customer会增加一个属性来维护其偏好的产品的id。
设置消费者偏好
/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == personId) &&
@ (\exists int i; 0 <= i && i < products.length; products[i].getId() == productId)
@ assignable getPerson(personId).products;
@ ensures (\forall Product i; \old(getPerson(personId).hasProduct(i));
@ getPerson(personId).hasProduct(i));
@ ensures getPerson(personId).hasProduct(productId);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer)
@ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < products.length; products[i].getId() == productId);
*/
public void setPreference(int personId, int productId) throws PersonIdNotFoundException, ProductIdNotFoundException;
发送广告
/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && people[i].containsAdvertisement(advertisementId));
@ assignable people[*].messages
@ ensures (\forall int i; 0 <= i && i < people.length; (getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length) + 1 && people[i].messages[0] == \old(getPerson(id).getAdvertisement(advertisementId)) && (\forall int j; 1 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j - 1]))));
@ ensures (\forall int i; 0 <= i && i < people.length; !(getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length && (\forall int j; 0 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j]))));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < people.length; people[i].getId() != id || (people[i].getId() == id && !people[i] instanceof Advertiser));
@ signals (AdvertisementIdNotFoundException e) (\exists int i; 0 <= i && i < people.length && people[i].getId() == id && people[i] instanceof Advertiser; !people[i].containsAdvertisement(advertisementId));
*/
public void sendAdvertisement(int id, int advertisementId) throws PersonIdNotFoundException, AdvertisementIdNotFoundException;
查询销售额
/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer);
@ requires (\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
@ \results == getProduct(productId).cost * (\sum int i; 0 <= i && i < people.length && getPerson(id).isLinked(people[i]) && (\exists int j; 0 <= j && j < people[i].products.length; prople[i].products[j].equals(getPerson(id).getProduct(productId))); 1);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer)
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
*/
public /*@ pure @*/ int querySalaryValue(int id, int productId) throws PersonIdNotFoundException, ProductIdNotFoundException;
学习体会
通过本单元的学习,我初步理解了契约式编程。契约式编程的核心思想是对软件系统中的元素(如类、方法)之间相互合作以及“责任”与“权利”的比喻。使用契约式编程,可以提高程序的鲁棒性,便于测试,便于组织模块间通信与多人协作。
以此类推,契约式思想还可以应用到我们的生活中。在多人合作的任务中,我们也可以采用契约的思想,使用更加规格化的语言来厘清每个人的责任与权利,提高合作的效率。
关于单元作业,本单元作业相较于前两个单元轻松了很多,顶层架构都已经由规格规定,我们要做的就是忠实地实现规格的要求。需要注意的是,规格并没有规定我们实现的具体方法,因此需要我们自己根据时空复杂度限制来设计合适的实现方法。
关于单元实验,本单元的实验我认为比作业更有价值,在实验中介绍了java垃圾回收的机制,考察了我们在短时间内阅读规格并实现代码、阅读代码并归纳出规格的能力。
关于建议,我认为本单元作业对JML的考察要求其实不高,根据讨论区和身边的同学的情况来看,大家似乎更多地把注意力放到算法和数据结构上了,并没有怎么关心JML本身。所以感觉可以适当增加三单元实验课的次数,增加在有限的时间内独立写JML的机会。