一、如何利用JML规格来准备测试数据:
- 根据每个方法的normal_behavior,exceptional_behavior及其require,进行初步分类。保证测试数据覆盖到所有不同的情况。同时,在每个normal_behavior中有若干种不同的情况,再对每一种normal_behavior中的情况进行单独讨论与测试。
- 根据junit生成的检测,保证每种方法都能涵盖到。对于不同的选择分支的测试,使用Junit自动生成测试框架,之后对于各种抛出异常的情况和不同的分支情况编写测试样例,尽量做到每一个方法的各种分支都进行了测试。
- 分析jml中每一条ensure以及invariant,设置数据专门检测每一个条件以及其边界状况。
二、梳理架构设计,分析自己的图模型构建和维护策略:
-
架构设计:根据指导书中的提示建立各个异常类以及MyNetwork,MyMessage,MyGroup,MyPerson。同时,在涉及最短路径,最小生成树算法时,由于并查集实现的要求,设置了Bridge,Side,Tree个类,以实现上述算法。在MyNetwork,MyMessage,MyGroup,MyPerson中较为简单的方法基本上根据jml语言可以直接写出,故在此不过多赘述。
-
模型建构及维护策略:对与最短路径,最小生成树,以及路径搜索算法,在此维护了一个并查集,通过维护两个ArrayList,每个ArrayList中的元素与存Person的ArrayList相对应。其中一个用于存每个Person的父节点,当没有relation时默认为其本身,另一个用于存深度,保证所有Person找其父节点的路径最短。每当有新的Relation加入时,查询其两端的Person,若二者最顶端的的父节点不相同,则查询二者父节点不同,则将两个父节点中深度浅的结点的父节点设置为深度深的结点。通过维护这样的数据结构能够较为快速的找到连通的Person的集合。向关代码可见“第一次作业性能问题”。
三、性能问题和修复情况:
- 第一次作业:寻找路径,以及图的分支数量。 一开始使用的是DFS算法,该算法较易于实现,思路较为简单,但复杂度较高,故而在之后放弃了该方法转而使用并查集算法进行实现,通过维护两个ArrayList装入父节点以及深度,动态地维护图的各个分支,同时采用路径压缩。从而大大减小了运算的复杂度,省去了递归。
private final ArrayList<Integer> pre = new ArrayList<>();
private final ArrayList<Integer> rank = new ArrayList<>();
public void addRelation(int id1, int id2, int value) throws
PersonIdNotFoundException, EqualRelationException {
...
int leadNum1 = methods.findLeader(people.indexOf(getPerson(id1)));
int leadNum2 = methods.findLeader(people.indexOf(getPerson(id2)));
if (leadNum1 != leadNum2) {
//pre.set(leadNum1, leadNum1);
if (rank.get(leadNum1).equals(rank.get(leadNum2))) {
rank.set(leadNum1, rank.get(leadNum1) + 1);
pre.set(leadNum2, leadNum1);
} else if (rank.get(leadNum1) > rank.get(leadNum2)) {
pre.set(leadNum2, leadNum1);
} else {
pre.set(leadNum1, leadNum2);
}
}
}
public int findLeader(int num) {
int personNum = num;
int supNum = pre.get(personNum);
while (supNum != pre.get(supNum)) {
supNum = pre.get(supNum);
}
return supNum;
}
-
第二次作业:采用克鲁斯卡尔算法通过排列所有边从而得到最小生成树。因为之前的并查集以及实现了对图的各个分支的维护,此时只需要新增一个ArrayList用于存储所有的边即可。将属于该分支的边选出再排列大小顺序,从而选出最小生成树。在这里并没有出现复杂度的问题。但发现在第一次作业中存在一个不必要的多重遍历,到置复杂度大大上升,这是由一个函数选择参数失误导致的,传参数前将index转为元素,而进入函数后又将元素转为index,修改后直接传递index即可。
for (Tree tree : trees) { if (tree.checkPerson(side.getPerson1()) && tree.checkPerson(side.getPerson2())) { flag = 1; break; } else if (tree.checkPerson(side.getPerson1())) { endpoints.add(tree); } else if (tree.checkPerson(side.getPerson2())) { endpoints.add(tree); } }
此外在计算group的value时也出现了复杂度问题,由于循环过多导致超时。解决方案则是为每个group维护一个value在添加person,relation时增加value,减少person时删除value
public void addPerson(Person person) { people.add(person); for (Person j : people) { if (person.isLinked(j)) { totalvalue += (2 * person.queryValue(j)); } } } public void newvalue(MyPerson p1, MyPerson p2, int value) { if (people.contains(p1) && people.contains(p2)) { totalvalue += (2 * value); } } public void delPerson(Person person) { for (Person j : people) { if (person.isLinked(j)) { totalvalue -= (2 * person.queryValue(j)); } } people.remove(person); }
-
第三次作业:第三次作业中性能问题主要集中与最短路径算法。最短路径算法中采取迪杰斯特拉算法正好处于超时的边缘,于是在该算法的基础上采取了更改容器的办法使得速度加快。将边使用PriorityQueue存储,并在Bridge中设置比较方法。从而使得每一条边加入时能够快速的通过value置升序排列的方法进入队列中,使得时间复杂度小幅提升,满足性能要求。
public int getPathM(Person start, Person dest, ArrayList<Person> branchPeople) {
HashSet<Integer> finished = new HashSet<>();
PriorityQueue<Bridge> paths = new PriorityQueue<>();
paths.add(new Bridge(start.getId(), 0));
while (true) {
int newDown = paths.element().getPersonId();
int newSP = paths.element().getDst();
paths.remove();
if (newDown == dest.getId()) {
return newSP;
}
if (!finished.contains(newDown)) {
finished.add(newDown);
Person person = hashpeople.get(newDown);
for (int i = 0; i < ((MyPerson) person).getAquaintance().size(); i++) {
int personId = ((MyPerson) person).getAquaintance().get(i).getId();
int value = ((MyPerson) person).getValues().get(i);
if (!finished.contains(personId)) {
paths.add(new Bridge(personId, newSP + value));
}
}
}
}
}
四、对Network进行扩展:
-
新增类:
Product类:产品类
/*@ public instance model int id; // 产品id @ public instance model int price; // 价格 @ public instance model int producerId; // 生产商id @ public instance model int salescount; // 表示产品的销量 @*/
Advertiser继承 Person
Producer继承 Person
Customer继承 Person
AdvertiseMessage继承 Message
- 新增属性product表示宣传产品
PurchaseMessage继承 Message
- 新增属性product表示购买产品
1.发布广告 advertise:
Advertiser将向所有关联的Customer发送信息。
/*@ public normal_behavior @ requires containsMessage(id) && getMessage(id) instanceof AdvertiseMessage @ assignable messages; @ assignable getMessage(id).getPerson1().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() && @ ensures (\forall int i; 0 <= i && i < people.length; (\old(getMessage(id)).getPerson1().isLinked(people[i]) && people[i] instanceof Customer) ===> (\forall int j; 0 <= j && j < \old(people[i].getMessages().size()); @ people[i].getMessages().get(j+1) == people[i].getMessages().get(j))); @ ensures (\forall int i; 0 <= i && i < people.length; (\old(getMessage(id)).getPerson1().isLinked(people[i]) && people[i] instanceof Customer) ===> (people[i].getMessages().get(0).equals(\old(getMessage(id))))); @ ensures (\forall int i; 0 <= i && i < people.length; (\old(getMessage(id)).getPerson1().isLinked(people[i]) && people[i] instanceof Customer) ===> (people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1)); @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); public void advertise(int id) throws MessageIdNotFoundException;
2.查询销售额查询商品销售额 searchProductSale
根据某productId查询其销售额。/*@ public normal_behavior @ requires containsProduct(id) @ ensures (\exists int i; 0 <= i && i < productIdList.length && productIdList[i] == id; \result == productSaleList[i]); @ also @ public exceptional_behavior @ signals (ProductIdNotFoundException e) !containsProduct(id); @*/ public /*pure*/ int searchProductSale(int id) throws ProductIdNotFoundException;
3.购买产品 purchase
customer找到对应广告信息,然后advertiser向producer发送信息。其中getAdvertisement()方法是根据productId返回person.messages中首个关于该产品的广告信息,hasAdvertisement()则是根据productId判断是否有该产品的广告信息。
/*@ public normal_behavior @ requires contains(pid) && getPerson(pid) instanceof Customer @ requires containsMessage(mid) && getMessage(mid) instanceof PurchaseMessage @ requires getPerson(pid).hasAdvertisement(getPerson(pid).productId) && getPerson(pid).getAdvertisement(getPerson(pid).productId).getPerson1().equals(getMessage(mid).getPerson1()) @ assignable messages; @ assignable getMessage(mid).getPerson1().socialValue; @ assignable getMessage(mid).getPerson2().socialValue; @ assignable productSaleList; @ ensures !containsMessage(mid) && 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(mid)).getPerson1().getSocialValue() == @ \old(getMessage(mid).getPerson1().getSocialValue()) + \old(getMessage(mid)).getSocialValue() && @ \old(getMessage(mid)).getPerson2().getSocialValue() == @ \old(getMessage(mid).getPerson2().getSocialValue()) + \old(getMessage(mid)).getSocialValue(); @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size()); @ \old(getMessage(mid)).getPerson2().getMessages().get(i+1) == \old(getMessage(mid).getPerson2().getMessages().get(i))); @ ensures \old(getMessage(mid)).getPerson2().getMessages().get(0).equals(\old(getMessage(mid))); @ ensures \old(getMessage(mid)).getPerson2().getMessages().size() == \old(getMessage(mid).getPerson2().getMessages().size()) + 1; @ ensures (\exist int i; 0 <= i && i < productIdList && productIdList[i] == getPerson(pid).productId; productSaleList[i] == \old(productSaleList[i]) + 1) @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(pid); @ signals (NotCustomerException e) contains(pid) && !(getPerson(pid) instanceof Customer) @ signals (MessageIdNotFoundException e) !containsMessage(mid); @*/ public void purchase(int pid, int mid) throws MessageIdNotFoundException,PersonIdNotFoundException,
五、学习体会:
jml语言为用户提供接口方法调用的规格,同时也指导了方法本身的设计,很好的隔开了用户调用与具体的实现。通过这个单元的学习我明白了对于一个编程任务,编写过程并不是最重要的,也不是最花时间的。而设计是很重要的。通过契约编程的思想,将设计由自然语言变为JML规格,使得其更为清晰,并且可以由工具来检查规格与实现的不一致。这样进一步分离了设计与实现。JML规格通过谓词逻辑与java自带的保证正确性的函数来描述自己程序的行为,从效果入手,这给了与实现者不一样的视角,有助于更好的理解自己的程序,写出bug少的程序。