北航oo第三单元博客作业
北航oo第三单元总结
基于JML规格的测试策略
第三单元的作业中需要支持的指令数相比前两个单元大大增加,同时每条指令都会收到JML规格的约束,在特定的情况下会有特定的输出或者是特定的异常抛出,并且运行过程中还不能超出最大时间的限制,因此本单元的测试策略我主要分成了两个部分:正确性测试和压力测试。
-
正确性测试
正确性测试主要是检测每条指令是否能正确地运行。在测试的方法上课程组是推荐使用JUnit来进行测试,但是我在使用JUnit的过程中觉得比较复杂,最后还是采用了根据JML规格,对于每条指令的每种情况自动生成特定数据进行检查的方法。正确性测试的关键在于要对每一条指令,对每一条指令中的不同情况都进行测试,不能遗漏任何一种情况。
-
压力测试
压力测试则是对复杂度较高的一些指令进行反复地测试,检查运行时间是否会超出最大时间限制。在这个部分我主要是对一些实现过程中复杂度较高的算法如检查两点是否连通、最小生成树和最短路径算法进行了测试,我通过python来进行大量数据的投喂,最后记录下完成所有输出后CPU的总运行时间,以此来避免算法实现复杂度过高。
架构设计
第三单元的架构方面主要是根据官方包的要求对相应的接口进行实现,除此之外还新建了两个类:Edge和Disjoint类,分别完成图模型的构建和并查集的实现。
-
图模型构建策略
图模型的构建策略比较简单,将每一个Person视作一个节点,当在两个Person之间添加acquaintance时则是给两个节点加上了一点边,最后我将所有的边都记录在了Edge类中,实现了图模型的构建。
-
维护策略
连通集合的维护策略,我选择了一个ArrayList作为容器和两个HashMap作为容器建立起并查集,两个HashMap记录下了每个Person的id和其在ArrayList中的index,实现了两者的互相转化,ArrayList则保存了每一个Person所在的连通集合的父结点。在加入Person时便将新的Person作为独立的节点加入到ArrayList中,在合并两个节点时则将两个连通集合合并,更新它们的父结点。这样查询两点是否连通时便可以直接查询他们的父结点是否相同即可。
最小生成树的维护策略,我选择的是使用并查集优化的Kruskal算法,由于已经实现过并查集,这里实现Kruskal算法也会相对比较简单,每次只需要取权值最小的边,利用并查集判断是否构成环,如果构成了环则抛弃当前边取下一条边,直至生成一棵树。
最短路径的维护策略,我使用的是堆优化的Dijkstra算法,利用的是PriorityQueue队列作为容器存储每个节点,最后返回两个节点的最短路径。
性能问题和修复情况
-
第九次作业
第九次作业因为建立起了并查集,同时在加入节点和加入边的时候就进行了并查集的更新,因此实现了查询两点是否连通时O(1)的复杂度。同时还使用了一个变量blocksum记录下图中块的总数,在加入节点时blocksum加1,合并两个节点时blocksum减1,因此qbs指令查询时直接返回blocksum变量即可,时间复杂度依旧是O(1)。本次作业由于使用了并查集,时间复杂度都很低,在强测中也没有出现任何问题。
-
第十次作业
第十次作业使用了并查集优化的kruskal算法,时间复杂度相对来说也比较低,因此在强测中也没有出现什么问题。但是在互测中却由于qgvs指令被hack烂了,这个指令我直接按照JML规格使用两重循环实现查询,但是 O(n^2)的时间复杂度会导致超时,因此在修复时我在加人和删人的时候都对该指令进行了动态维护,从而实现O(1)的时间复杂度。
-
第十一次作业
第十一次作业我一开始只使用了普通的Dijkstra算法,没有使用堆优化,导致了强测的时候有一个点超时了,因此在修复过程中加入了PriorityQueue实现了堆优化的Dijkstra算法,修复了超时的问题。
Network扩展和相应的JML规格
-
Network扩展
可以将Advertiser、Producer和Customer作为Person类的子类加入到Network之中,然后还可以加入advertiseMessage、produceMessage等作为Message类的子类。业务功能的接口方法中也可以加入queryCustomerSum、queryProductValue、sendAdvertiseMessage等方法。
-
JML规格
我选择对queryCustomerSum、queryProductValue和sendAdvertiseMessage三个方法实现相应的JML规格。
//@ ensures \result == (\sum int i; 0 <= i && i < people.length && people[i] instance of Customer; 1) public /*@ pure @*/ int queryCustomerSum();
/*@ public normal_behavior @ requires containsProduct(productId) @ ensures \result == getProduct(productId).getValue(); @ also @ public exceptional_behavior @ signals (ProductIdNotFoundException e) !containsProduct(productId); @*/ public /*@ pure @*/ int queryProductValue(int productId) throws ProductIdNotFoundException;
/*@ public normal_behavior @ requires containsAdvertiseMessage(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().messages, getMessage(id).getPerson2().socialValue; @ 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(); @ 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).getPerson2().getMessages().size()); @ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i))); @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0) == \old(getMessage(id)); @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1; @ also @ public normal_behavior @ requires containsAdvertiseMessage(id) && getMessage(id).getType() == 1 && @ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()); @ assignable people[*].socialValue, messages; @ ensures (\forall Person p; getMessage(id).getGroup().hasPerson(p); p.getSocialValue() == @ \old(p.getSocialValue()) + getMessage(id).getSocialValue()); @ ensures (\forall int i; 0 <= i && i < people.length && !getMessage(id).getGroup().hasPerson(people[i]); @ \old(people[i].getSocialValue()) == people[i].getSocialValue()); @ 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])))); @ also @ public exceptional_behavior @ signals (AdvertiseMessageIdNotFoundException e) !containsAdvertiseMessage(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 sendAdvertiseMessage(int id) throws RelationNotFoundException, AdvertiseMessageIdNotFoundException, PersonIdNotFoundException;
学习体会
本单元体会最深的就是对JML规格语言,契约式编程相关内容的学习。我学习到了如何使用JML语言来表达编程中的语言,比如我们求和操作使用\sum来表达,循环操作\forall、存在操作\exists等等。并且由于JML对接口行为、功能和规格作出描述和规定,因此我们在完成程序的时候能够很清晰地理解设计者设计每个接口方法的意图,以及每一个接口方法完成的任务。
在完成作业的过程中,首先得根据JML规格的要求,完全相应的任务,在保证了正确性的前提下,还要尽可能地设法降低算法的复杂度,并且这一单元对我们的性能要求也相比前两个单元来说高了不少。通过这一单元的学习,JML规格的约束在很大程度上也规范我对于每个方法的编写,加深了我对模块化编程,工程化编程的理解,收获颇丰。