BUAA OO Summary - Unit3
一、自测利用JML规格准备测试数据
白盒测试
这一部分其实不涉及构造数据,但是确是至关重要的。个人感觉在写完所有代码后,只有自己把代码对应JML规格每个方法重新梳理对照一遍,才能一定程度上保证这次作业的正确性——这是来自白盒测试的安全感。
由于本单元的正确性很大程度取决于对JML规格的正确理解,若理解不正确造出来数据后得到输出,也不能判断代码正确与否,所以我的三次作业的测试数据都是通过仔细阅读并理解每个方法并对其正常与异常情况尽可能覆盖地进行构造数据来完成。
其中仔细观察JML的描述,会些小坑点,例如
-
addToGroup
有人数上限,不得超过1111
黑盒测试
有了白盒测试,基本保证了代码按照自己所理解的JML规格来运行,但是,很多时候,可能问题不出在理解上,而出在自己的代码能力上,也就是自己写的代码没有按照自己所期望的方向运行。
此外由于本单元作业虽然没有性能分,但是个人感觉对性能的要求其实更硬性了,因为这直接决定最后测试点的正确性,性能不好直接tle。而对于性能好坏的评测,除了通过计算代码复杂度,最简单的办法,就是在满足测试数据限制的前提下构造大量且集中攻击复杂度高的方法的数据,然后输入程序来看得到输出耗时的长短,如果输进去以后要卡一段时间才能出结果,那多半性能就是不行的,而想要解决这种性能问题,除去利用一些例如isUpdated的标志位来保证不重复计算已经计算过的值的傻瓜方法以外,多半是需要将整个方法重新再写一遍的,而这个问题集中体现在
-
第一次作业
-
isCircle
——qci -
queryBlockSum
——qbs
-
-
第二次作业
-
queryGroupValueSum
-
queryLeastConnection
——qlc
-
-
第三次作业
-
sendIndirectMessage
——sim
-
通过ap,ar,ag,atg等指令构造一个复杂的人际关系网后,将这些指令反复调用,排列组合,在一定指令书数下最大化地反复调用复杂度高的方法,就很容易卡掉时间,是互测hack别人的好办法。
此外除了有可能tle以外,如果没有投入数据进行测试,很难发现一些自己看起来很对,怎么也检查不出来,但是其实蒟蒻的bug,比如
for(int i=0; i<list.size(), i++) {
list.remove(1);
}
很多类似的问题,我一般是经过仔细阅读代码白盒测试过了以后,投入数据后报NullPointer的异常了之后我才后知后觉,汗。
其他方法,还包含课程组强推的JUnit,但是这个也需要在理解JML规格的情况下才能写的测试代码的正确性,JUnit的好处很多,可以知道测试到具体的语句,以及测试覆盖度等,是个很好的测试工具。
二、 架构设计——图模型构建和维护策略
第一次作业
|-- exceptions
| |-- MyEqualGroupIdException.java
| |-- MyEqualPersonIdException.java
| |-- MyEqualRelationException.java
| |-- MyGroupIdNotFoundException.java
| |-- MyPersonIdNotFoundException.java
| |-- MyRelationnIdNotFoundException
| `-- Timer.java
|-- main
| |-- MyGroup.java
| |-- MyNetwork.java
| `-- MyPerson.java
|-- MainClass.java
第二次作业
|-- exceptions
| |-- MyEqualGroupIdException.java
| |-- MyEqualMessageIdException.java //new
| |-- MyEqualPersonIdException.java
| |-- MyEqualRelationException.java
| |-- MyGroupIdNotFoundException.java
| |-- MyMessageIdNotFoundException.java //new
| |-- MyPersonIdNotFoundException.java
| |-- MyRelationnIdNotFoundException
| `-- Timer.java
|-- main
| |-- MyGroup.java
| |-- MyMessage.java //new
| |-- MyNetwork.java
| `-- MyPerson.java
|-- MainClass.java
第三次作业
|-- exceptions
| |-- MyEmojiIdNotFoundException.java //new
| |-- MyEqualEmojiIdException.java //new
| |-- MyEqualGroupIdException.java
| |-- MyEqualMessageIdException.java
| |-- MyEqualPersonIdException.java
| |-- MyEqualRelationException.java
| |-- MyGroupIdNotFoundException.java
| |-- MyMessageIdNotFoundException.java
| |-- MyPersonIdNotFoundException.java
| |-- MyRelationnIdNotFoundException
| `-- Timer.java
|-- main
| |-- MyEmojiMessage.java //new
| |-- MyGroup.java
| |-- MyMessage.java
| |-- MyNetwork.java
| |-- MyNoticeMessage.java //new
| |-- MyPerson.java
| `-- MyRedEnvelopeMessage.java //new
|-- MainClass.java
三、性能问题与修复情况
第一次作业
第一次作业相对简单,但是由于一开始我对于JML规格对实现代码的约束关系不是很明确,所以就几乎是一比一还原了JML的描述。但是写到后面,我开始意识到如果按照JML的描述原封不动地实现,虽然能保证输出结果正确,但是复杂度很高,性能很差。所以我开始明白了JML只是从规格上约束,而我们可以通过不同的数据结构和算法在满足规格的情况下实现。
所以比如在实现isCircle
方法的时候,一开始我几乎一比一还原了规格,复杂度极高
@Override
public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
if (contains(id1) && contains(id2)) {
return judgeCircle(id1, id2);
} else {
if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (contains(id1) && !contains(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
return false;
}
}
private boolean judgeCircle (int id1, int id2) {
HashSet<Integer> circleSet = new HashSet<>();
circleSet.add(id1);
Iterator iter1 = people.entrySet().iterator();
while (iter1.hasNext()) {
Map.Entry entry1 = (Map.Entry) iter1.next();
int newId = ((MyPerson) entry1.getValue()).getId();
if (newId!=id1) {
Iterator iter2 = circleSet.iterator();
while (iter2.hasNext()) {
Integer tmpId = (Integer) iter2.next();
if (getPerson(newId).isLinked(getPerson(tmpId))) {
circleSet.add(newId);
break;
}
}
}
}
return circleSet.contains(id2);
}
而随后,我利用了并查集以后,代码也不冗余了,且复杂度很低十分高效,这也让我意识到这一单元算法的重要性,核心代码如下
private int findFa(int id) {
//路径压缩
if (fathers.get(id) == id) {
return id;
}
fathers.put(id, findFa(fathers.get(id)));
return fathers.get(id);
}
@Override
public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
if (contains(id1) && contains(id2)) {
return findFa(id1) == findFa(id2);
} else {
if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else {
throw new MyPersonIdNotFoundException(id2);
}
}
}
而这里的fathers需要在addPerson中实现:
fathers.put(person.getId(), person.getId());//初始化老大为自己
在addRealtion中实现:
fathers.put(findFa(id1), findFa(id2));//id2打败了id1
第二次作业
第二次作业其他方法都较容易,不太需要什么算法来优化,这一次作业最重要的是queryLeastConnection
方法,这主要考察最小生成树算法,这里我运用的是prim算法,按照如下规则:
-
将连通网中的所有顶点分为两类(假设为 A 类和 B 类)。初始状态下,所有顶点位于 B 类;
-
选择任意一个顶点,将其从 B 类移动到 A 类;
-
从 B 类的所有顶点出发,找出一条连接着 A 类中的某个顶点且权值最小的边,将此边连接着的 A 类中的顶点移动到 B 类;
-
重复执行第 3 步,直至 B 类中的所有顶点全部移动到 A 类,恰好可以找到 N-1 条边。
核心实现如下:
int ans = 0;
HashSet<Integer> aclass = new HashSet<>();
HashSet<Integer> bclass = new HashSet<>();
//先找和id在一个联通分支的人
Iterator iter = people.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Integer tmpId = (Integer) entry.getKey();
if (findFa(tmpId) == findFa(id)) {
bclass.add(tmpId);
}
}
aclass.add(id);
bclass.remove(id);
while (bclass.size() != 0) {
int minEdge = 1001;
int minIdOfB = 0;
int tmpEdge = 0;
for (Integer apersonId : aclass
) {
for (Integer bpersonId : ((MyPerson) getPerson(apersonId)).getAcquaintance()
) {
if (bclass.contains(bpersonId)) {
tmpEdge = getPerson(bpersonId).queryValue(getPerson(apersonId));
if (tmpEdge < minEdge) {
minEdge = tmpEdge;
minIdOfB = bpersonId;
}
}
}
}
aclass.add(minIdOfB);
bclass.remove(minIdOfB);
ans += minEdge;
}
第三次作业
第三次作业最难最重要的同样是——容易因为实现方法不同而性能、复杂度有差异的sendIndirectMessage
方法,这里面主要涉及到最短路算法,我选择的是Dijkstra算法,实现如下,但是由于没有进行堆优化,导致强测和互测都出现了超时的问题。
private int shortestRoad(int id1, int id2) {
//if (!shortestWeightMap.containsKey(id1)) {
//}
//return shortestWeightMap.get(id1).get(id2);
HashMap<Integer, Integer> shortestWeight = new HashMap<>();
HashMap<Integer, Boolean> found = new HashMap<>();
MyPerson startPerson = (MyPerson) getPerson(id1);
HashSet<MyPerson> block = blockMap.get(findFa(id1));
Iterator<MyPerson> iter1;
for (int tmpId : startPerson.getAcquaintance()) {
if (block.contains((MyPerson) getPerson(tmpId))) {
shortestWeight.put(tmpId, startPerson.queryValue(getPerson(tmpId)));
}
}
shortestWeight.put(id1, 0);
found.put(id1, true);
int cur = id1;
for (int i = 0; i < block.size(); i++) {
int minWeight = Integer.MAX_VALUE;
iter1 = block.iterator();
while (iter1.hasNext()) {
MyPerson tmp = iter1.next();
Integer tmpId = tmp.getId();
if (shortestWeight.containsKey(tmpId) && (!found.containsKey(tmpId) || !found.get(tmpId))) {
if (shortestWeight.get(tmpId) < minWeight) {
cur = tmpId;
minWeight = shortestWeight.get(cur);
}
}
}
MyPerson curPerson = (MyPerson) getPerson(cur);
found.put(cur, true);
for (Integer tmpId : curPerson.getAcquaintance()) {
MyPerson tmp = (MyPerson) getPerson(tmpId);
if (block.contains(tmp)) {
int tmpValue = minWeight + getPerson(cur).queryValue(tmp);
if ((!found.containsKey(tmpId) || !found.get(tmpId))
&& (!shortestWeight.containsKey(tmpId) || tmpValue < shortestWeight.get(tmpId))) {
shortestWeight.put(tmpId, tmpValue);
}
}
}
if (found.containsKey(id2)) {
return shortestWeight.get(id2);
}
}
return -1;
}
四、Network扩展
根据要求扩展Network:
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
要实现Advertiser发广告,Producer卖东西以及Customer买东西;所以从需要扩展的类或接口来看:
-
首先需要增加一个
AdvertisementMessage
接口, -
然后再增加
Advertiser
、Producer
和Customer
接口继承Person接口。 -
最后在实现的时候,创建MyAdvertisementMessage类实现AdvertisementMessage接口,
-
再创建
MyAdvertiser
、MyProducer
、MyCustomer
类分别实现Advertiser
、Producer
和Customer
接口。 -
然后在Producer中
-
设置销售产品的方法sellProduct,并配置属性salesNum代表销售额
-
-
在Customer中
-
设置购买产品的方法
buyProduct
以及受到广告的方法recieveAdvertisement方法,并配置属性boughtProduct
(可以提现消费者偏好)以及advertisements
属性存储收到的广告recievedAdertisements
-
-
在Advertiser中
-
设置发送产品广告的方法
sendAdvertisement
-
-
最后为了可以在人机网络中关联流通,还需要再network中增加
addAdvertisementMessage
的方法(其实可以直接扩展addMessage的方法,为了方便一会单独写JML规格),设置sendAdvertisementMessage
方法,设置customerBuyProduct
方法。 -
此外network中还应该配备
querySalesNum
等查询方法以了解对象的状态
addAdvertisementMessage
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
@ (message.getType() == 0) && (message.getPerson1() != message.getPerson2()) &&
@ (message.getPerson1() instanceof Advertiser) && (message.getPerson2() instanceof Customer);
@ 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 (EqualPersonIdException e) !(\exists int i; 0 <= i && i < messages.length;
@ messages[i].equals(message)) &&
@ ((message instanceof EmojiMessage) ==>
@ containsEmojiId(((EmojiMessage) message).getEmojiId())) &&
@ message.getType() == 0 && message.getPerson1() == message.getPerson2();
@*/
public void addAdvertisementMessage(AdvertisementMessage message) throws
EqualMessageIdException, EqualPersonIdException;
sendAdvertisementMessage
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages;
@ assignable getMessage(id).getPerson1().socialValue;
@ assignable getMessage(id).getPerson2().recievedAdertisements, getMessage(id).getPerson2().socialValue;
@ 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 \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue(); //两个人的socialValue都要增加
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().get.recievedAdertisements().size());//所有消息往后,也就是插入开头
@ \old(getMessage(id)).getPerson2()..recievedAdertisements().get(i+1) == \old(getMessage(id).getPerson2().recievedAdertisements().get(i)));
@ ensures \old(getMessage(id)).getPerson2().recievedAdertisements().get(0).equals(\old(getMessage(id))); //第二个人的messages的第一个是该消息
@ ensures \old(getMessage(id)).getPerson2().recievedAdertisements.size() == \old(getMessage(id).getPerson2().recievedAdertisements.size()) + 1;//消息数加一
@ also
@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 1 &&
@ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
@ assignable people[*].socialValue, people[*].money, messages, emojiHeatList;
@ 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])))); //消息从network中去掉
@ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
@ \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue()); //network中在消息所在群的所有人的socialValue都要增加
@ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
@ \old(people[i].getSocialValue()) == people[i].getSocialValue()); //network中不在消息所在群的人socialValue不变
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
@*/
public void sendAdvertisementMessage(int id) throws
RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;
querySalesNum
/*@ public normal_behavior
@ requires contains(id) && (getPerson(id) instanceof Producer);
@ ensures \result == getPerson(id).getSalesNum();
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id);
@*/
public /*@ pure @*/ int querySalesNum(int id) throws PersonIdNotFoundException;
五、本单元学习体会
本单元学习了JML规格,感受到了即便有JML规格的约束,代码实现仍旧十分灵活,使用不同的数据结构和不同的算法,虽然最后结果的正确性都能得到保证,但是性能会大相径庭,这也让我意识到了在拿到JML规格后,不能觉得各个方法已经被设计好了而略过了架构的设计,其实这时候架构的设计仍然很重要,这将极大影响后面各方法实现的复杂度以及整体的性能。此外,也让我了解到学好数据结构和算法这两个基础的重要性。
此外也学会了可以通过使用JUnit来辅助进行测试,从而进一步保证代码实现的正确性。