BUAA_OO_UNIT3_BLOG_JML
BUAA_OO_UNIT3_BLOG_JML
一、摘要
JML单元相较前两单元显得轻松了一些,从 JML 到 Java 更像是一个翻译的过程。只要按照官方给出了的 JML 撰写代码,正确性似乎难度较小,但是较大的数据量对算法的复杂度发出了 “无声” 的要求(OO 算法课),很多精力都放在 RTLE 和 CTLE 上。
二、测试
测试采用了 随机测试 + 边界测试的方法。
-
随机测试。采用了随机生成的数据测试,基本可以保证程序的正确性。
-
边界测试。通过极端数据,如最大量的算法指令或 Group 超 1111的数据进行测试,防止 RTLE 的出现。
(对拍找出了一些bug)
for i in range(100):
print("\n\t Now is data" + str(i))
os.system("time java -jar ../" + str(p+1) +".jar < ../data"+str(i)+".txt > output" + str(i) + ".txt")
os.system("diff ../output" + str(i) +".txt output"+str(i)+".txt > diff-"+str(i)+".txt")
三、架构设计
三次作业各引入了一种算法。
第九次作业:不相交集合问题
采用了并查集算法,使用的数据结构是
private final ArrayList<HashSet<Person>> map = new ArrayList<>();
Hashset 中会覆盖重复的元素,在并查集算法中使用更加便捷、性能更好。
社交网络中,每一条 ap 指令(add person)都会在 Arraylist 中增加一个存放该 Person 的 Hashset 。
ar 指令(add relation)涉及并查集的维护,以下给出一个实例流程:
即将两个元素所在的集合合并。
第十次作业:最小生成树问题
算法:
最小生成树常见的算法有如下两种:
-
Prim算法,对节点操作,找和节点集合最近的点
-
Kruskal算法,对边操作,找最短边
考虑到测试数据量较大,需要对边集进行更新,因此选择了对边操作更加便捷的 Kruskal 算法,并用并查集的算法去维护边集。
(实测,Prim算法 + 不维护边集,一组强测限制下的数据需要 900 s)
此外,新建了一个 Side 类,存储边两端节点和长度等信息。
边集的更新时机:
-
边的添加和边集的合并:这一部分添加到了上一次作业的算法中。
-
边的删除(更新):Kruskal 算法每执行一次,对应边集中,没有用到的边将会删除。
(最小生成树现在未用到的边,将来也不会用到)。
数据结构:
ArrayList<ArrayList<Side>> sides
和不相交问题不同,为了便于排序并且不会存在相同的边,内部也使用了 ArrayList
容器。
第十一次作业:最短路径问题
采用了 迪杰斯特拉算法(未进行堆优化,完全图为最坏情况,时间复杂度为O(n^2))。MyNetwork
类超出了 checkstyle
代码行数的限制,所以新建了一个 Algorithm 类
,三次作业的算法都放在其中并设置为 static
。
数据结构:
HashMap<Person, Integer> path = new HashMap<>(); //起始点到 Person 的路径长度
ArrayList<Person> flag = new ArrayList<>(); //标志 Person 是否被遍历过
ArrayList<Person> in = new ArrayList<>(); //找到路径的点
算法流程:
四、性能问题和修复情况
(褚老师 : 第三单元是拿分的好时机)
第九次作业:
强测、互测均未出现 bug。
第十次作业:
强测中出现两个测试点错误,互测时出现一个测试点错误,错误类型都是 RUNTIME_ERROR
bug分析:
没有考虑集合中只有一个人的情况,在维护并查集和边集时,并查集对应的边集会被删除,在之后维护边集时就可能会出现 IndexOutOfBoundsException。
bug修复:
在 MyNetwork
类中的 findLeastTree
方法中添加了对一个元素集合的特判。
第十一次作业:
强测中出现一个测试点 CPU 运行超时。
这个测试点的错误我不是很能理解:
-
CPU使用时间 > 总体运行时间 ???
-
本地运行这组数据时,只用了 4 秒,到服务器上却超时了 ???
五、NetWork 扩展
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等请讨论如何对Network扩展。
新的三类人,分别继承 Person 类, 并增加新的属性和方法。还需要一个新的广告类继承 Message。Network 中添加两个方法 queryVolume() 和 queryPath(Message advertise);
假设广告商可能有多层,即 生产商 ---> 广告商 ---> ... ---> 广告商 ---> 顾客
(以下 get 和 set 方法不完全)
public interface Advertiser extends Person {
private Person upper; //上一级广告商或生产商
public void sendRequest(Message advertise);
public Message sendAdvertise();
}
public interface Producer extends Person {
private Type volume;
public Type queryVolume();
public void produce();
}
public interface Customer extends Person {
private type[] preference;
public void sendRequest();
public void setPreference();
}
public interface Advertise extends Message {
private type preference;
private Person advertiser;
private Person producer;
public Person getAdvertiser();
public Person getProducer();
}
核心业务方法:
-
Customer 中的
sendRequest
方法/*@ public normal_behavior
@ requires (messages.length > 0 && preferences.length > 0);
@ assignable nothing;
@ ensures (\forall int i; (0 <= i && i < messages.length) &&
@ (messages[i] instanceof Advertise) &&
@ (\exists int j; 0 <= j && j < preferences.length;
@ preferences[j] == ((Advertise)message[i]).getPreference);
@ (((Advertise)message[i]).getAdvertiser.sendRequest(message[i])));
@*/
public void sendRequest(); -
Advertise 中的
sendRequest
方法/*@ public normal_behavior
@ requires (upper instanceof Producer);
@ assignable nothing;
@ ensures ((Producer)upper).produce();
@ public normal_behavior
@ requires (upper instanceof Advertiser);
@ ensures ((Advertiser)upper).sendRequest();
@*/
public void sendRequest(); -
Network 中的
queryPath
方法/*@ public normal_behavior
@ requires (advertise instanceof Advertise);
@ assignable nothing;
@ ensures (\forall int i; 0 <= i && i < result.length;
@ (exists int j; 0 <= j && j < people.length; result[i] == people[j]);
@ ensures (\forall int i; 0 <= i && i < result.length - 1;
@ ((Advertiser)people[i].getUpper == people[i + 1];)
@ ensures result[0] == (Advertise)advertise.getAdvertiser();
@ also
@ public exceptional_behavior
@ requires !(advertise instanceof Advertise);
@ signals NotAdvertiseException;
@*/
public List<Person> sendRequest(Message advertise);
查询产品的销量实际就是查询对应 Producer 的 getVolume() 方法,不做具体描述。
六、学习体会
从几次实验和作业中,感受到了 JML ---> Java 翻译的快乐和 JML 攥写规格的痛苦,但是也逐渐理解了 JML 和 规格存在的意义。
-
契约式编程。对契约式编程有了初步了解,契约将方法(类)分为两方,即甲方、乙方。甲方依照契约向乙方发送任务,乙方有权利检查甲方的任务是否符合契约,不符合则拒绝执行。附上一篇相关博客:
-
准确定义和表示方法的行为。如,在第一、第二单元中,架构设计较为复杂,类图只能描述架构的整体的框架,但细节到一些方法和规格,还是需要 JML 去进行准确定义、表示。
-
提供了测试设计的依据。JML 提供了方法的前置条件和后置条件,可以作为测试正确性的依据之一。测试时,仅通过黑箱测试,很难保证测试数据的覆盖率。如,作业中,Group 中的人数有一个上限 1111,这处细节用随机生成的数据进行黑箱测试很难发现,需要单元测试的协助。
-