OO第三单元总结
OO第三单元总结
任务概述
本单元作业的任务目标是通过实现一个社交关系系统,逐步理解JML规格及其在面向对象设计与构造中的意义,掌握使用JML规格提高代码质量的能力。
测试数据
本单元我主要是通过用Python搭评测机随机生成数据来进行测试。主要策略是先随机生成包含每个指令的数据,查找普遍性的bug,再根据JML规格集中对某个指令来生成针对性数据。如测试qlc指令时可以设计特定的人数和关系来进行验证。由JML的描述可以得知qlc是查找图中简单路径的指令,可以先手动设计不同情况的图,构造边界数据,并将其通过评测机转化成具体指令进行测试,也可以通过大规模随机数据进行测试。其他指令的测试也是类似的方法。
架构设计
1.Network类集合容器选择
private final HashMap<Person, Integer> people;
private final ArrayList<Group> groups;
private final List<Message> messages;
private final HashMap<Integer, Integer> emojis;
private final PriorityQueue<Edge> edges = new PriorityQueue<>();
对于people的存储采用Hashmap实现并查集,key为people,value为people对应的联通分量的其中一个节点的id。如id为1的people1加入id为2的people2所在联通分量,就将people1所在的联通分量的所有节点的value改成people2的value,每个人的初始value为自己的id。emoji的key为id,value为该emoji的popularity。
edge为people之间的关系集合,通过新建edge类来存储信息,每一个relation对应一条边,PriorityQueue的排序按照关系的value从小到大的顺序排列。具体实现如下:
public class Edge implements Comparable<Edge> {
private Person startPerson;
private Person endPerson;
private int value;
public Edge(Person startPerson, Person endPerson, int value) {
this.startPerson = startPerson;
this.endPerson = endPerson;
this.value = value;
}
public int getValue() {
return value;
}
public Person getEndPerson() {
return endPerson;
}
public Person getStartPerson() {
return startPerson;
}
@Override
public int compareTo(Edge edge) {
return this.value - edge.value;
}
}
2.图算法实现及维护
并查集查询联通分量
在hw9中qci和qbs的实现需要通过并查集来优化性能,查找person对应的联通分量时只需在people的hashmap中取出对应value即可判断出所在的连通分量,若两个key对应的value相同则处于相同的连通分量。
Kruskal算法求最小生成树
hw10中的qlc指令需要求连通图的最小生成树。具体实现通过创建工具类CalQ来计算最终结果。在传入参数时创建新的PriorityQueue并将该person所在的连通分支内的所有边都加入到这个优先队列中,将其作为参数初始化工具类,通过并查集来优化Kruskal算法复杂度。CalQ工具类具体实现如下,通过getSum函数求出最终结果。
import com.oocourse.spec2.main.Person;
import java.util.HashMap;
import java.util.PriorityQueue;
public class CalQ {
private PriorityQueue<Edge> edges;
private HashMap<Person, Person> father = new HashMap<>();
private HashMap<Person, Integer> people;
public CalQ(HashMap<Person, Integer> people, PriorityQueue<Edge> edges) {
this.people = people;
this.edges = edges;
}
public void getEdges(int id) {
for (Person person1 : people.keySet()) {
if (people.get(person1).equals(people.get(getPerson(id)))) {
father.put(person1, person1);
}
}
}
public int getSum() {
int sum = 0;
while (!edges.isEmpty()) {
Edge edge = edges.poll();
Person f1 = findFather(edge.getStartPerson(), father);
Person f2 = findFather(edge.getEndPerson(), father);
if (!f1.equals(f2)) {
sum += edge.getValue();
father.put(f1, f2);
}
}
return sum;
}
private Person findFather(Person person, HashMap<Person, Person> father) {
Person f = father.get(person);
if (!f.equals(person)) {
f = findFather(f, father);
}
father.put(person, f);
return f;
}
public Person getPerson(int id) {
for (Person person : people.keySet()) {
if (person.getId() == id) {
return person;
}
}
return null;
}
}
Dijkstra算法求最短路
hw11中的sim指令需要求带权图中两点之间最短加权路径,根据评论区中的思路实现了基于堆优化的Dijkstra算法。首先把距离数组dis中出发点s距离设置为0,其余点设置为一个超出数据范围的较大数,标记数组vis全部设置为0,把点s加入优先队列。之后每次从优先队列中弹出一个节点u,因优先队列的特性该节点权值最小,若u已经被标记,则不作操作,否则将对应标记数组的值设为1,取出它的距离值,遍历它的所有边,假设u与v相连,若v被标记过,不做操作,否则比较dis(u) + w(u, v)与dis(v),若前者较小,则将dis(v)改为dis(u) + w(u, v),并把v加入优先队列。该步骤停止条件为优先队列为空。
节点以及优先队列的优先级比较通过创建Node类来存储相关信息,并以Node类中的value值从小到大排序优先队列。具体实现如下:
import com.oocourse.spec3.main.Person;
public class Node implements Comparable<Node> {
private Person person;
private int value;
public Node(Person person, int value) {
this.person = person;
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Person getPerson() {
return person;
}
@Override
public int compareTo(Node node) {
return this.value - node.value;
}
}
计算sim的工具类CalSim类实现如下:
import com.oocourse.spec3.main.Person;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
public class CalSim {
private HashMap<Person, Integer> dis = new HashMap<>();
private HashMap<Person, Integer> flags = new HashMap<>();
private PriorityQueue<Node> nodes = new PriorityQueue<>();
private HashMap<Person, Integer> people;
public CalSim(HashMap<Person, Integer> people) {
this.people = people;
}
public void getNodes(Person p1) {
for (Map.Entry<Person, Integer> entry : people.entrySet()) {
if (!entry.getKey().equals(p1)) {
flags.put(entry.getKey(), 0);
dis.put(entry.getKey(), 15000);
} else {
Node node = new Node(entry.getKey(), 0);
dis.put(entry.getKey(), 0);
flags.put(entry.getKey(), 0);
nodes.add(node);
}
}
}
public void countDis() {
while (!nodes.isEmpty()) {
Node node = nodes.poll();
if (flags.get(node.getPerson()) == 0) {
flags.put(node.getPerson(), 1);
for (Person p : people.keySet()) {
if (node.getPerson().isLinked(p) && flags.get(p) == 0) {
if (dis.get(node.getPerson()) + node.getPerson().queryValue(p)
< dis.get(p)) {
dis.put(p, dis.get(node.getPerson()) +
node.getPerson().queryValue(p));
Node node1 = new Node(p, dis.get(node.getPerson()) +
node.getPerson().queryValue(p));
nodes.add(node1);
}
}
}
}
}
}
public int getDis(Person p) {
return dis.get(p);
}
}
通过getDis函数得到对应节点的距离,即最终结果。
出现的性能问题
在hw10强测中由于没有优化queryGroupValueSum导致出现CTLE。具体修复方法是向group加人时维护一个valueSum,即
@Override
public void addPerson(Person person) {
people.add(person);
for (Person p : people) {
if (person.isLinked(p)) {
valueSum += person.queryValue(p) * 2;
}
}
ageSum += person.getAge();
}
顺便维护ageSum。在删人时valueSum也要减去相应的值,
@Override
public void delPerson(Person person) {
ageSum -= person.getAge();
for (Person p : people) {
if (person.isLinked(p)) {
valueSum -= person.queryValue(p) * 2;
}
}
people.remove(person);
}
即可避免超时。
Network扩展
对于Advertiser、Producer和Customer继承Person接口,PurchaseMessage和Advertisement继承Message接口,新建Product类存储产品信息。Producer存储生产的产品信息,Customer存储偏好产品以及购买的产品信息。
三个核心业务功能:
Advertiser发送广告:
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 && getMessage(id) instanceof Advertisement
@ && getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2() &&
@ getMessage(id).getPerson1 instanceof Advertiser &&
@ getMessage(id).getPerson2() instanceof Customer;
@ assignable messages;
@ assignable getMessage(id).getPerson1().socialValue;
@ assignable getMessage(id).getPerson2().messages, 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();
@ 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).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ also
@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 1 && && getMessage(id) instanceof Advertisement
@ && getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()) &&
@ getMessage(id).getPerson1 instanceof Advertiser;
@ assignable people[*].socialValue, 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 Person p; \old(getMessage(id)).getGroup().hasPerson(p) && p instanceof Customer;
@ p.getSocialValue() == \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
@ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]) && people[i] instance of Customer;
@ \old(people[i].getSocialValue()) == people[i].getSocialValue());
@ also
@ public exceptional_behavior
@ signals (NotAdvertisementException e) !(getMessage(id) instanceof Advertisement);
@ signals (MessageIdNotFoundException e) !containsMessage(id) && getMessage(id) instanceof Advertisement;
@ signals (WrongPersonTypeException e) containsMessage(id) && getMessage(id) instanceof Advertisement &&
@ !(getMessage(id).getPerson1() instanceof Advertiser);
@ signals (WrongPersonTypeException e) containsMessage(id) && getMessage(id) instanceof Advertisement &&
@ getMessage(id).getPerson1() instanceof Advertiser &&
@ !(getMessage(id).getPerson2() instanceof Customer);
@ signals (RelationNotFoundException e) getMessage(id) instanceof Advertisement &&
@ containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2())) &&
@ getMessage(id).getPerson1() instanceof Advertiser &&
@ getMessage(id).getPerson2() instanceof Customer;
@ signals (PersonIdNotFoundException e) getMessage(id) instanceof Advertisement &&
@ containsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1())) &&
@ getMessage(id).getPerson1() instanceof Advertiser;
@*/
public void sendAdvertisement(int id) throws WrongPersonTypeException,
NotAdvertisementException, RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;
查询产品销售额:
/*@ public normal_behavior
@ requires containsProduct(id);
@ ensures \result == (\sum int i; 0 <= i && i < people.length && people[i] instanceof Producer
@ && people[i].containsProduct(getProduct(id)); people[i].getProductSales());
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(id);
@*/
public /*@ pure @*/ int queryProductSalesSum(int id) throws
ProductIdNotFoundException;
生产商生产产品:
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Producer &&
@ getPerson(id1).containsProduct(getProduct(id2));
@ assignable getPerson(id1).productSum;
@ ensures getPerson(id2).getProductSum(productId) ==
@ \old(getProducer(id1).getProductSum(id2)) + 1;
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id1);
@ signals (WrongPersonTypeException e) contains(id1) && !(getPerson(id1) instanceof Producer);
@ signals (WrongProductException e) contains(id1) && getPerson(id1) instanceof Producer &&
@ !getPerson(id1).containsProduct(getProduct(id2))
@*/
public void produceProduct(int id1, int id2) throws
PersonIdNotFoundException, WrongPersonTypeException, WrongProductException;
总结与心得体会
第三单元的重点在于理解JML规格,因此代码难度并不高。其中难度较高的图算法在讨论区也有同学给出提示,架构设计也只需按照官方包来继承接口,所以主要难点就是根据JML写出没有bug的规范化代码。
虽然JML看似很繁琐,并且很多内容是看似众所周知的,并不需要专门写出来,但是契约式编程的优势就在于只要按照正确的JML规格一步步写出代码,这样的代码是不可能出现bug的,代码正确度大大提高。尝试写JML也能锻炼我们的思维能力以及重视各项细节,能够考虑到每一个特殊情况。即使之后的学习不需要写JML规格,但是这样的锻炼可以让我们考虑问题时更加全面,提高准确度。第三单元对于图算法的要求也让我回顾了一部分大一数据结构的内容,也让我温故知新,对于图算法有了新的经验与感受。