BUAA_OO_Unit3 基于JML规格的设计总结
BUAA_OO_Unit3 基于JML规格的设计总结
一、综述
面向对象课程的第三单元的主题是基于JML规格的设计与测试。本单元的作业背景是实现简单的社交网络,包括NetWork,Group,Person,Message等元素,通过添加图算法和异常类,来实现查询和异常处理功能。本次作业已经通过JML规格给出了主体架构,意在考察对JML规格的理解和代码实现以及测试能力。
二、作业分析
1. Homework 9
1.1 UML类图
1.2 分析
1.2.1 架构设计
本次作业主要实现MyGroup、MyNetwork、MyPerson三个类,实现简单社交关系的模拟和查询,学习目标为 入门级JML规格理解与代码实现。
Network是顶层的社交网络,即顶层图结构(无向图)。其中每个Person对象为图的点,Person之间的acquaintance关系为图的带权边,边权值为value。在此之外有个Group的额外结构,可以理解为Network的子图。直接采用层次化管理的方式,Network管理Person和Group,记录查询所有的信息;Group管理Group中的Person,Person则负责管理邻接的Person和边权value,记录查询自身信息。
1.2.2 性能优化
-
容器选择
由于对于每个Person都有独一无二的id,每个Group也有独一无二的id,并且在查询式并不用考虑对象的顺序,所以考虑用
Hashmap<id, Objiect>
的形式进行存储容器,从而达到把查找的复杂度降到O(1)的目的。同时,对于每个Person对象,每个acquaintance关系对应一个value,所以可以采用
Hashmap<id, value>
的形式存储每个Person对象邻接对象的id和相应权值。这样存储可以避免用两个容器取存储这两个对象。 -
并查集优化
本次作业需要降低复杂度的两条方法是MyNetwork里的
isCircle(int id1, int id2)
和queryBlockSum()
。根据JML规格解释,isCircle(int id1, int id2)
方法用来判断id1和id2对应的两个Person是否连通,queryBlockSum()
返回Network里连通分支的个数。这里的连通查询不需要具体的连通路径,所以可以采用并查集来实现上述的两个方法。建立一个新的并查集类
JointSearch将其作为MyNetwork类的属性。
并查集的实现如下,进行了路径压缩和按秩合并:
private int blockSum; //连通分支数目 private final HashMap<Integer, Integer> parent; //记录根节点的容器 private final HashMap<Integer, Integer> rank; //记录秩的容器 /*添加Person对象*/ public void addPerson(Person person); /*查找根节点*/ public int find(int id); /*按秩合并*/ public void merge(int id1, int id2); /*返回查询连通结果*/ public boolean isCircle(int id1, int id2); /*返回连通分支数*/ public int queryBlockSum();
当加入新的Person对象,由于这个Person对象没有和其他Person对象建立关系(没有连接),所以多了一个新的连通分量,
blockSum++
;当添加新的关系时,如果两个对象处在不同的连通分支上,则进行合并,blockSum--
;按秩合并可以避免树的高度过高,导致在一定数据下查找效率过低的情况。最后通过判断两个结点的根节点是否相等,判断是否连通,使得
isCircle(int id1, int id2)
方法复杂度降为O(log(2)(n)),利用维护的量blockSum
使得queryBlockSum()
方法复杂度降为O(1)。
2. Homework 10
2.1 UML类图
2.2 分析
2.2.1 架构设计
本次作业在第九次作业的基础上加入了新的Message类,并且增加了Network和Person的一些功能,同时在Network增加了查询Group属性的方法。整体架构和第九次作业没有什么区别,增添了Person和Network对Message对象的存储和管理。笔者额外建立了Edge类用来存储Network的边。
2.2.2 性能优化
-
容器选择
和第九次作业的选择一样,通过
Hashmap<id, Objiect>
的形式存储可以快速实现查询,不过对于Person对象管理下的Message,由于Message的存储有顺序关系,并且需要在Message的队列头添加元素,选择用LinkedList<Message>
进行存储。 -
查询方法
本次作业在Network增加了查询Group属性的方法,包括
queryGroupAgeVar(int id)
,queryGroupValueSum(int id)
。如果每次查询都通过遍历来计算的话,复杂度就会变成O(n)或者O(n^2),所以笔者通过在Group中维护三个变量valueSum(2倍权值总和),ageSum(年龄总和),ageSquareSum(年龄平方的总和)。
维护时需要注意:
- valueSum:往Group加(减)Person时,将Group中和其邻接的Person的之间边权值的2倍从valueSum加入(减去);添加关系(value)时,如果添加关系的两个对象都在Group中,需要
valueSum += 2 * value
。 - ageSum:往Group加(减)Person时,需要将其年龄从ageSum加(减)。
- ageSquareSum:往Group加(减)Person时,需要将其年龄的平方从ageSum加(减)。
$$
ageMean = (∑age(i)) / n = ageSum / n
$$$$
ageVar = ∑ (age(i) - ageMean)^2 = (ageSquareSum + n * ageMean * ageMean - 2 * ageMean * ageSum)/n
$$使得上述变量的查询复杂度降为O(1);
- valueSum:往Group加(减)Person时,将Group中和其邻接的Person的之间边权值的2倍从valueSum加入(减去);添加关系(value)时,如果添加关系的两个对象都在Group中,需要
-
Kruskal优化
根据阅读
queryLeastConnection(int id)
的JML规格(读了很久),发现该方法就是实现一颗最小生成树,由于在第九次作业中实现了并查集,所以这里采用Kruskal算法:private final ArrayList<Edge> edges; //存储连通图的所有边 private final HashMap<Integer, Integer> parent; //记录根节点的容器 private final HashMap<Integer, Integer> rank; //记录秩的容器 /*在Network中遍历,添加属于该连通分量的边*/ public void addEdge(Edge edge); /*在Network中遍历,添加属于该连通分量的Person对象*/ public void addPerson(Person person); /*查找根节点*/ public int find(int id); /*按秩合并*/ public void merge(int id1, int id2); /*返回查询连通结果*/ public boolean isCircle(int id1, int id2); /*返回最小生成树的边权和*/ public int leastConnection();
其中,在求最小生成树的边权和时,需要将权值从小到大排序,我重写了Edge类的
compareTo()
方法,调用Collections.sort()
进行排序。
3. Homework 11
3.1 UML类图
3.2 分析
3.2.1 架构设计
本次作业在第十次作业的基础上增加了多种Message,即新增了NoticeMessage、RedenvelopMessage、EmojiMessage三个Message的子类。在整体架构上没有改变太多。
3.2.2 性能优化
-
容器选择
本次作业在第十次作业的基础上添加了EmojiHeat这一概念,其和emojiId是一一对应的,所有采用
Hashmap<emojiId, Times>
的方法进行存储。 -
Dijkstra算法优化
根据
sendIndirectMessage(int id)
的JML规格,需要实现一个求最短路径的方法,笔者采用Dijkstra算法并进行了堆优化:private HashMap<Integer, Person> people; //Network中的所有Person对象 private final HashMap<Integer, Boolean> vis; //判断结点是否已经被连通 private final HashMap<Integer, Integer> dis; //最短路径存储 private final PriorityQueue<Node> priority; //优先队列 private HashMap<Integer, ArrayList<Edge>> edgeTable;//所有的边,Key值为PersonId,value为以其为顶点的所有边 private static final int INF = 0x3f3f3f3f; /*初始化*/ public void initial(HashMap<Integer, Person> people, int id, JointSearch jointSearch, HashMap<Integer, ArrayList<Edge>> edgeTable); /*计算最短路*/ public int leastValueSum(int id1, int id2);
三、性能分析和测试
1. 性能分析
在前两次作业中,笔者对算法的优化都花了不少心思,所以在强测和互测中都没有问题,但是在第三次作业中出现了由于性能问题被卡了一个点的情况,下面进行分析和修复情况。
public int leastValueSum(int id1, int id2) {
priority.add(new Node(id1, 0));
dis.put(id1, 0);
while (!priority.isEmpty()) {
Node node = priority.poll();
int id = node.getSide();
if (vis.get(id)) {
continue;
}
vis.put(id, true);
if (vis.get(id2)) {
return dis.get(id2);
}
!!!ArrayList<Edge> temp = edgeTable.get(id);
for (Edge e : temp) {
int q;
if (e.getEndId() != id) {
q = e.getEndId();
} else {
q = e.getStartId();
}
if (dis.get(q) > node.getValue() + e.getValue()) {
dis.put(q, e.getValue() + node.getValue());
priority.add(new Node(q, dis.get(q)));
}
}!!!
}
return dis.get(id2);
}
上面的感叹号处,笔者在修复前是对Person进行遍历,导致进行的堆优化没有用到,复杂度还是O(n^2),将这里改为对边进行遍历,就用到了堆优化,复杂度降为O((m+n)logn);
2. 测试
本单元作业采用黑盒测试和白盒测试两种测试方法,主要方式是采用和同学对拍的方式。
白盒测试:和同学进行代码逻辑的对拍,不考虑容器和具体实现方式,只对拍逻辑结构和所有的逻辑路径,保证理解的JML规格没有偏差;
黑盒测试:通过自动评测机进行对拍。通过自动数据生成器生成随机数据,将数据分别给同学的代码和自己的代码测试,然后对结果进行对拍,检验代码的正确性。
经过上诉两部分的测试后,代码的逻辑已经几乎没有什么问题了,然后进行强数据构造进行性能测试和边缘数据。比如第十次作业queryGroupAgeVar(int id)
,queryGroupValueSum(int id)
的复杂度,第十一次作业最短路算法的优化等,可以通过不断调用的方式,让高复杂度的方法不断占用CPU;然后就是一些敏感的细节数据,比如第十次作业Group成员的1111上限,很多同学没有注意,可以构造超过1111人进入Group的数据。
四、Network扩展
要求:假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格。
Advertiser、Producer和Customer都继承Person类,新增类advertisement继承Message类。
Producer类有一个属性products,用来存储生产产品信息,不同的产品用id区分。
生产产品
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
(getPerson(id) instanceof Producer) &&
getProducer(id).hasProduct(productId);
@ assignable getProducer(id).productCounts;
@ ensures getProducer(id).getProductCounts(productId) ==
@ \old(getProducer(id).getProductCounts(productId)) + 1;
@ also
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
(getPerson(id) instanceof Producer) &&
!getProducer(id).hasProduct(productId);
@ assignable getProducer(id).products,
getProducer(id).productCounts;
@ ensures (\exists int i; 0 <= i && i < getProducer(id).products.length;
getProducer(id).products[i] == productId &&
getProducer(id).productCounts[i] == 1);
@ ensures getProducer(id).products.length ==
\old(getProducer(id).products.length) + 1 &&
@ getProducer(id).productCounts.length ==
\old(getProducer(id).productCounts.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(getProducer(id).products.length);
@ (\exists int j; 0 <= j && j < getProducer(id).products.length;
getProducer(id).products[j] == \old(getProducer(id).products[i]) &&
@ getProducer(id).productCounts[j] ==
\old(getProducer(id).productCounts[i])));
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
people[i].getId() == id);
@ signals (NotProducerException e) (\exists int i; 0 <= i && i < people.length;
people[i].getId() == id) &&
!(getPerson(id) instanceof Producer);
@*/
public void produceProduct(int id, int productId) throws
PersonIdNotFoundException, NotProducerException;
发送广告
/*@ public normal_behavior
@ requires containsMessage(id) && (getMessage(id) instanceof Advertisement) &&
getMessage(id).getProuctId == productId;
@ assignable messages;
@ assignable people[*].messages;
@ 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 < people.length &&
!getMessage(id).getPerson1().isLinked(people[i]);
@ people[i].getMessages().equals(\old(people[i].getMessages()));
@ ensures (\forall int i; 0 <= i && i < people.length &&
getMessage(id).getPerson1().isLinked(people[i]);
@ (\forall int j; 0 <= j && j < \old(people[i].getMessages().size());
@ people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) &&
@ people[i].getMessages().get(0).equals(\old(getMessage(id))) &&
@ people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1);
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (NotAdvertisementException e) containsMessage(id) &&
!(getMessage(id) instanceof Advertisement);
@ signals (WrongAdvertisementException e) containsMessage(id) &&
(getMessage(id) instanceof Advertisement) &&
!(getMessage(id).getProuctId == productId);
@*/
public void sendAdvertisement(int id, int productId) throws MessageIdNotFoundException, NotAdvertisementException, WrongAdvertisementException;
为消费者添加喜欢的产品
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) &&
(getPerson(id) instanceof Customer) && !getCust(id).hasProduct(productId);
@ assignable getCust(id).products;
@ ensures (\forall Product i; \old(getCust(id).hasProduct(i));
@ getCust(id).hasProduct(i));
@ ensures \old(getCust(id).products.length) == getCust(id).products.length - 1;
@ ensures getCust(id).hasProduct(getProduct(productId));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id);
@ signals (NotCustomerException e) (\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id) &&
!(getPerson(id) instanceof Customer);
@ signals (EqualProductException e) (\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id) &&
(getPerson(id) instanceof Customer) &&
getCust(id).hasProduct(productId);
*/
public void setPreference(int id, int productId) throws PersonIdNotFoundException,
NotCustomerException, EqualProductException e;
五、体会与感想
本单元作业的难度相比于前两个单元难度下降了不少,并且三次作业之间的迭代更新更加过渡自然。由于整体的架构已经由JML规格给出,就不需要在设计架构上花太多时间。最重要的是阅读和理解JML规格,并且能够将JML规格转化成正确的代码描述,最终能够自己写JML规格。
在进行了本单元的学习后,不仅学会了JML规格,更是在潜移默化中让自己的代码有了逻辑性、模块化。异常类的加入,JML规格的描述,使得代码的逻辑分支更加清晰,在很大程度上保证的代码的正确性。
对于如何阅读JML也是产生了新的经验,需要在代码规格和JML规格之外加上自然语言规格,即用自然语言描述JML规格辅助从JML到java代码的转换。