OO第三单元总结

OO第三单元总结

一、根据JML规格构造测试数据

  • 由于JML规格自身描述的清晰性,只要正确遵循JML规格去进行代码撰写,一些简单直白的方法只需要手动构造基础样例进行基本测试即可。
  • 针对异常的测试,这需要考虑到一些极端且易混淆的情形。比如第一次作业中的点与自己之间的一些情形:

qv 1 1 //查看一个人和它自己之间边的权值
qci 1 1 //查看一个人与它自己之间是否连通
ar 1 1 123 //不能给自己加关系,必须返回异常;同时这是id1==id2的情形,计数只能添加一次而不是两次

异常报错的先后顺序,如下列方法

public void addToGroup(int id1, int id2) throws MyGroupIdNotFoundException,
MyPersonIdNotFoundException, MyEqualPersonIdException{....}

有多个异常时,报错的先后顺序也可能出错。

  • JML规格描述和实际算法实现之间可能会造成TLE问题。如第二次作业中的qgvs指令可能会造成TLE,第一次作业中计算连通分量的算法,第三次作业中求最短路的算法,这些可以通过构造大量重复性数据去发现。

二、架构设计及图模型的构建和维护策略

架构设计

按照所给的JML规格即可实现大部分功能,其他新增的一些细节如下:

Count类

用于处理所有的异常总计数情形,以及建立了相应的HashMap,用以统计某个id所导致的异常的次数。在增加新的异常种类时,只需添加相应的计数变量,HashMap,还有add()方法即可,十分方便。

public class Count {
private static int pinfAll = 0;
private static int epiAll = 0;
private static int rnfAll = 0;
private static int erAll = 0;
.......
private static HashMap<Integer, Integer> Whos_pinf = new HashMap<>();
private static HashMap<Integer, Integer> Whos_epi = new HashMap<>();
private static HashMap<Integer, Integer> Whos_rnf = new HashMap<>();
private static HashMap<Integer, Integer> Whos_er = new HashMap<>();
......
public int getPinfAll() {
return pinfAll;
}
public int getEpiAll() {
return epiAll;
}
public int getRnfAll() {
return rnfAll;
}
public int getErAll() {
return erAll;
}
......
public HashMap<Integer, Integer> getWhoSpinf() {
return Whos_pinf;
}
public HashMap<Integer, Integer> getWhoSepi() {
return Whos_epi;
}
public HashMap<Integer, Integer> getWhoSrnf() {
return Whos_rnf;
}
public HashMap<Integer, Integer> getWhoSer() {
return Whos_er;
}
......
public void addpinfAll(int id) {
pinfAll = pinfAll + 1;
if (Whos_pinf.get(id) == null) {
Whos_pinf.put(id, 1);
} else {
Whos_pinf.replace(id, Whos_pinf.get(id) + 1);
}
}
public void addepiAll(int id) {
epiAll = epiAll + 1;
if (Whos_epi.get(id) == null) {
Whos_epi.put(id, 1);
} else {
Whos_epi.replace(id, Whos_epi.get(id) + 1);
}
}
public void addrnfAll(int id1, int id2) {
rnfAll = rnfAll + 1;
if (Whos_rnf.get(id1) == null) {
Whos_rnf.put(id1, 1);
} else {
Whos_rnf.replace(id1, Whos_rnf.get(id1) + 1);
}
if (Whos_rnf.get(id2) == null) {
Whos_rnf.put(id2, 1);
} else {
Whos_rnf.replace(id2, Whos_rnf.get(id2) + 1);
}
}
public void adderAll(int id1, int id2) {
erAll = erAll + 1;
if (Whos_er.get(id1) == null) {
Whos_er.put(id1, 1);
} else {
Whos_er.replace(id1, Whos_er.get(id1) + 1);
}
if (id2 != id1) {
if (Whos_er.get(id2) == null) {
Whos_er.put(id2, 1);
} else {
Whos_er.replace(id2, Whos_er.get(id2) + 1);
}
}
}
......
}

Edge类

用来表示邻接表里面的边,用以在第二次作业中,对prim算法进行邻接表+堆优化,减轻时间复杂度。

(而JML规格里面,MyPerson类里面的acquaintance数组和values数组其实起到了用邻接数组表示图的作用)

public class Edge implements Comparable {
private int toId; //被指向的顶点的id
private int weight; //边的权值
private Edge next; //下一段边
public Edge(int toid, int weight, Edge next) {
this.toId = toid;
this.weight = weight;
this.next = next;
}
public int getToId() {
return toId;
}
public void setToId(int toId) {
this.toId = toId;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Edge getNext() {
return next;
}
public void setNext(Edge next) {
this.next = next;
}
public int compareTo(Edge other) {
if (this.getWeight() < other.getWeight()) {
return -1;
} else if (this.getWeight() > other.getWeight()) {
return 1;
}
return this.getToId() - other.getToId();
//如果边的权值一样,也不好返回0,就返回id作差吧
}
}

图模型的构建和维护策略

邻接数组:利用MyPerson类里面的acquaintance数组和values数组

邻接表:利用新增的Edge类

并查集:新增一下以下数组,用于求连通分量的个数

private static int[] Father = new int[10000];

新增名为IdToNumber的Hashmap,用于表示点的id到点的编号的映射,方便用在并查集算法,prim算法,dijkstra算法之中。
private static HashMap<Integer, Integer> IdToNumber = new HashMap<>();
public void addRelation(int id1, int id2, int value) throws
MyPersonIdNotFoundException, MyEqualRelationException {
if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (contains(id1) && !contains(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
/if (id1 == id2) {
return;
} /
else if (getPerson(id1).isLinked(getPerson(id2)) || id1 == id2) {
throw new MyEqualRelationException(id1, id2);
}
((MyPerson) getPerson(id1)).getAcquaintance().add(getPerson(id2));
((MyPerson) getPerson(id2)).getAcquaintance().add(getPerson(id1));
((MyPerson) getPerson(id1)).getValue().add(value);
((MyPerson) getPerson(id2)).getValue().add(value);
join(IdToNumber.get(id1), IdToNumber.get(id2)); //加入并查集中
/

下面把边关系加入邻接表中
/
Edge edge1 = new Edge(id2, value, null);
if (((MyPerson) getPerson(id1)).getNext() == null) {
((MyPerson) getPerson(id1)).setNext(edge1);
} else {
Edge lastEdge = ((MyPerson) getPerson(id1)).getNext();
while (lastEdge.getNext() != null) {
lastEdge = lastEdge.getNext();
}
lastEdge.setNext(edge1);
}
/

无向图相当于同一边,两个点都互相指着对方,所以再加一次
/
Edge edge2 = new Edge(id1, value, null);
if (((MyPerson) getPerson(id2)).getNext() == null) {
((MyPerson) getPerson(id2)).setNext(edge2);
} else {
Edge lastEdge = ((MyPerson) getPerson(id2)).getNext();
while (lastEdge.getNext() != null) {
lastEdge = lastEdge.getNext();
}
lastEdge.setNext(edge2);
}
Person person1 = getPerson(id1); //先提前写出来,防止下面group每一次判断hasPerson时都要花时间遍历people数组一次
Person person2 = getPerson(id2);
for (Group group : groups) { /
这个就是防止qgvs指令TLE,在给两个人加边的时候, /
if (group.hasPerson(person1) && group.hasPerson(person2)) { /
看看这两个人是否已经在同一组中 /
((MyGroup) group).setMyvalueSum(((MyGroup) group).getMyvalueSum() + value);
} /
如果在,那就更新该组的valueSum 每次遍历一次组,都是O(n),不会超时
/
}
}

三、性能问题和修复情况

本次作业中,主要的性能问题都出现在了TLE上。

第一次作业

qbs指令:查询图中的所有连通分量的个数。

原先的算法:dfs,两层循环里面调用isCircle()函数判断任意两点是否连通,来累加连通分量个数。时间复杂度O(n^2),TLE。

public boolean isCircle(int id1, int id2) throws MyPersonIdNotFoundException {.....}
/isCircle():判断两个点是否连通/
public int queryBlockSum() {
int sum = 0;
for (int i = 0; i < people.size(); i++) {
int flag = 0;
for (int j = 0; j < i; j++) {
try {
if (isCircle(people.get(i).getId(), people.get(j).getId())) {
flag = 1;
break;
}
} catch (MyPersonIdNotFoundException e) {
e.print();
}
}
if (flag == 0) {
sum += 1;
}
}
return sum;
}

改进:利用并查集算法,时间复杂度降为O(n)

用某个结点所在连通分量的根节点的编号,代表这个连通分量的编号。

查询连通分量时,遍历所有结点,如果这个结点的根节点是其自身,则说明这个结点是其所在连通分量的根节点。统计有多少个这样的结点,即为图中连通分量的个数。

同时,因为结点的id为int,范围太大,不好处理,所以建立一个HashMap<Integer, Integer> IdToNumber

其意义为:该id的点是第几个加入到图中的点。

用后者作为点的编号去指代某个点,而不是用点的id去指代某个点。

 private static int[] Father = new int[10000];
private static HashMap<Integer, Integer> IdToNumber = new HashMap<>();

public MyNetwork() {
for (int i = 0; i < 10000; i++) {
Father[i] = i; //初始化,每个节点的根节点是它自己
}
}
private int find(int x) {
int r = x;
while (Father[r] != r) {
r = Father[r];
}
int i = x;
int j;
while (i != r) {
j = Father[i];
Father[i] = r;
i = j;
}
return r;
}
void join(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
Father[fx] = fy;
}
}
public void addPerson(Person person) throws MyEqualPersonIdException {
.......
IdToNumber.put(person.getId(), people.size() - 1); //建立从id到这是第几个人之间的映射
}
public void addRelation(int id1, int id2, int value) throws
MyPersonIdNotFoundException, MyEqualRelationException {
.............
join(IdToNumber.get(id1), IdToNumber.get(id2)); //加入并查集中
}
public int queryBlockSum() {
int sum = 0;
for (int i = 0; i < people.size(); i++) {
int tooli = IdToNumber.get(people.get(i).getId());
if (Father[tooli] == tooli) {
sum++;
}
}
return sum;
}

第二次作业

TLE之处:qgvs指令。

该指令意义:对于某个Group,获取其所有点之间的所有边的权值之和的2倍。

JML规格中给出的写法的时间复杂度是O(n^2),而每个组最多1111个人,对于这1111个人,如果每次qgvs指令时都重新计算result,那么会消耗大量时间,造成TLE。

改进方法:在MyGroup类里面维护一个属性valueSum,其代表的就是qgvs指令所求的result。

每次往组里加人减人,或者给组里已有的两个人添加边时,就更新一下valueSum。qgvs指令时直接返回valueSum*2的值就行了。

改进后的代码:

//给组里已有的两个人添加边时,更新valueSum
public void addRelation(int id1, int id2, int value) throws
MyPersonIdNotFoundException, MyEqualRelationException {
........
Person person1 = getPerson(id1); //先提前写出来,防止下面group每一次判断hasPerson时都要花时间遍历people数组一次
Person person2 = getPerson(id2);
for (Group group : groups) {
if (group.hasPerson(person1) && group.hasPerson(person2)) {
((MyGroup) group).setMyvalueSum(((MyGroup) group).getMyvalueSum() + value);
}
}
}
//往组里加人减人时,更新一下valueSum
public void addPerson(Person person) {
......
for (Person person1 : people) {
if (person1.isLinked(person)) {
myvalueSum += person1.queryValue(person);
}
}
return;
}

public void delPerson(Person person) {
......
for (Person person1 : people) {
if (person1.isLinked(person)) {
myvalueSum -= person1.queryValue(person);
}
}
return;
}

Group和Network有区别,后者才是传统意义上的图。Group相当于在图中圈了几个点而已,甚至点之间都不一定处于同一个连通分量中。

第三次作业

犯了个错误:

混淆了sendmessage()方法和sendindirectmessage()方法。

前者是是两个人之间必须直接有边相连,发消息,否则会报异常(RelationNotFoundException);

后者是两个人只要连通就行,不一定直接有边相连,这样发送消息。

发消息部分的代码,两个方法几乎一模一样,但是为了方便,我在sendindirectmessage()方法里面又调用了sendmessage()方法,所以老误报错,报RelationNotFoundException异常。

同时,这样导致本该发出去的indirectmessage()消息发不出去(因为去报异常RelationNotFoundException了,故发消息+从messages里面删除信息就不会执行)

导致本该发了然后删除的信息,没发也没删,下次再加入这条信息时,本该正常加入(因为前面本该已经删除了),却报异常(EqualMessageIdException)

改进:

其实sendindirectmessage()方法里面发消息的代码和sendmessage()方法里面type==0情形的代码是完全相同的,
完全复制粘贴即可,如果调用sendmessage()反而会报不必要的异常。

四、Network拓展

题目:

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

答:

首先在Network类中增加这三种数据维护:

@ public instance model non_null Advertiser[] advertisers;
@ public instance model non_null Producer[] producers;
@ public instance model non_null Customer[] customers;

选择如下三种方法接口:

  • 查询某个产品的销售额:

/@ public normal_behavior
@requires containsProduct(proId);
@ assignable nothing;
@ ensures \result == (\sum int i; 0 <= i && i < producers.length && producers[i].hasProduct(getProduct(proId));
@ producers[i].getProductSales(proId));
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(proId);
@
/
public /@ pure @/ int queryProductSalesSum(int proId) throws
ProductIdNotFoundException;

  • 消费者增加对某产品的偏好:

/@ public normal_behavior
@ requires contains(customerId);
@ requires containsProduct(proId);
@ ensures getCustomer(customerId).likes(proId) == true;
@
/
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(proId);
@/
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundException e) !contains(customerId);
@
/
public /@ pure @/void saleProduct(int customerId, int proId) throws
ProductIdNotFoundException, CustomerIdNotFoundException;

  • 得到某产品的价格

/@ public normal_behavior
@ requires containsProduct(proId);
@ ensures \result == getProduct(proId).getPrice();
@
/
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(proId);
@/
public /
@ pure @*/ int ProductPrice(int proId) throws
ProductIdNotFoundException;

五、学习体会

  • 我觉得JML规格的本质更像是一种”约定俗成的注释“。用自然语言去描述代码要求,表面上简单易懂,但是随着工程复杂性的增加,考虑到表达者和接受者不同的语言习惯,以及自然语言的变化和差异,那么理解出现偏差是难以避免的,双方势必都要花费大量宝贵的时间去交流,以达成认知的纠错和统一。为了避免不必要的麻烦,所以才会诞生出基于JML规格的契约化编程方法,通过将自己的需求翻译成JML规格这门新语言,准确严谨去表达出自己的意思。

  • 发展JML规格的目的就是形成一种严谨的,能准确无误表达出所有逻辑的统一性语言,但它也是一把双刃剑。为了消除模糊性,做到真正的严谨,JML规格同时也会变得抽象,舍弃了一定的易读性。(比如本单元第二次作业中的qlc指令,仅凭JML的描述很难搞懂其含义,而自然语言“生成该点所位于的连通分支的最小生成树”就一目了然。)同时,完全按照JML规格的描述去撰写代码,可能会造成性能的损失,实际写代码时要在理解JML规格含义的前提下,灵活变通优化。

  • 总之,没有哪一种语言能够一次性精准清晰表达出所有意思。无论JML规格再怎么完善,它的表达性仍然是有欠缺的,要和自然语言互为补充地去看。

posted @ 2022-06-06 15:57  朱睿达  阅读(49)  评论(0编辑  收藏  举报