OO-2022-Unit3-BeihangCSE
第三单元总结
数据测试
副作用
在JML中,一个方法除了返回值即\result之外,还有存在许多限制条件,例如\not_assigned, \not_modified,这些条件通常限制了我们在一个方法内对被调用的对象的操作,这是测试数据的重点之一
异常处理
需要对\require 做出的对 normal_behaviour 和 exceptional_behaviour做到全面的测试。需要构造各种各样的情况以全面覆盖方法行为。通常而言,exceptional_behaviour 下规定的抛出异常的情况相对特殊且固定,是很容易想到的。我们还需要注意在 normal_behaviour 中可能存在的“不便处理”或者“需要特殊处理”的情况,这是在编程中容易出现错误的地方。从编程者的角度而言,需要思考自己的方法能否应对 normal_behaviour 下存在的所有情况。
架构设计与性能维护
架构的建设
总体上说,在类的设计上并没有多少我们能发挥的空间,大部分只需要按照文档新建一个My*类并且实现对应接口中的方法即可。
但在方法的实现和类管理数据的策略上,JML并不做要求。
在一开始看到类似Person[] people
的描述,我下意识地会使用ArrayList甚至数组来管理数据。但会发现这样的数据结构在后续若干查询操作中会显得非常复杂,最后不得已还是选择了使用HashMap。
但其实这并不能说是“不得已”。正如前文所说,JML并不限制我们的实现方法,Person[] people
这样的表述也不过只是要求了一个“容器”,在不要求顺序的场景下,任何的容器都是合理的。在频繁查询的需求下,使用HashMap是自然的事。
hw9中的并查集
hw9中存在一个查询联通的指令,isCircle
,以及一个在JML中依靠它完成描述的指令,queryBlockSum
,查询联通分支的数量。
isCircle
的实现,我们很自然能想到使用BFS或者DFS等算法搜索整个图来实现。由于指导书中限制了相关指令的次数,这种O(V+E)的算法是合理的。但如果在此基础上还要按照JML的描述使用双重循环遍历来实现queryBlockSum
那就非常糟糕了。
为了优化整体的性能,我选择了并查集+动态维护queryBlockSum
的方法来实现两个方法。要实现并查集,那么我们还需要另外几个数据结构和方法,具体如下
private HashMap<Integer,Integer> branches; //父节点
private HashMap<Integer, Integer> rank; //深度
private static int size; //blockSum
public int find(int i) { //查找根节点
if (branches.get(i) == i) {
return i;
}
else {
return find(branches.get(i));
}
}
public void union(int i, int j) { //合并点
int b1 = find(i);
int b2 = find(j);
if (rank.get(b1) <= rank.get(b2)) {
branches.put(b1, b2);
if (rank.get(b1).equals(rank.get(b2)) && b1 != b2) {
rank.put(b2, rank.get(b2) + 1);
}
} else { branches.put(b2, b1); }
if (b1 != b2) {
size--;
}
}
在该算法中,我还添加了rank这个结构来尽量平衡树,使得复杂度缩减到了Olog2V。
hw10中的动态维护与Prim堆优化
在hw10中,query_least_connection
,是一个比较明显的高复杂度的指令。在讨论区,助教也给出了相应的算法,
但值得注意的是,Prim算法是O(V2),Kruskal算法的复杂度是O(ElogV)。指导书中的数据限制中,我们可以知道E<<V2,因此Prim算法是存在风险的。最好使用堆优化。而且Java自带优先队列PriorityQueue
这一数据结构,所谓堆优化实现起来是非常方便的。
下面就是这次作业最容易被忽略的query_group_value_sum
。因为它的实现相当简单,很多同学下意识就按照JML给出的方式实现了,丝毫没有意识到它的O(V2)的复杂度。而且指导书中并没有对相关指令做限制,所以很容易就出现超时,这也是在互测阶段中大多数同学出问题的原因
解决办法也很简单,在atg\dtg\ar等指令中动态维护一个groupValueSum即可。
hw11中的最短路径
hw11并没有什么需要特别注意的,只要使用Dijkstra实现sendIndirectmessage
就好。当然,为了保险起见,最好加上之前使用过的堆优化。
边的表示
为了实现Dijkstra和Prim算法,我另外设计了一个类Link
public class Link implements Comparable<Link> {
private int id1;
private int id2;
private int value;
public Link(int id11, int id21, int value1) {
id1 = id11;
id2 = id21;
value = value1;
}
public boolean containsTo(int id) { return id2 == id; }
public int getTo() { return id2; }
public int getValue() { return value; }
public int compareTo(Link link) {
return value - (link.getValue()) > 0 ? 1 : -1;
}
}
并且在MyPerson中实现了一个方法返回它的所有Link
public ArrayList<Link> allLink() {
ArrayList<Link> links = new ArrayList<>();
for (Integer i : acquaintance.keySet()) {
links.add(new Link(id, acquaintance.get(i).getId(), value.get(i)));
}
return links;
}
关于代码规模
有些同学在最后依次作业中遇到了MyNetwork代码超过500导致checkStyle出问题的情况,为了缩减代码花了很多心思。在此我提供几个思路:
- 边、并查集等“工具”,可以在抽象到单独的类中实现,不必放在MyNetwork来实现其操作。甚至类似于Prim、Dijkstra等算法也可以单独封装到另一个类的方法实现,只要提供一个接口给MyNetwork访问即可。
- 动态维护的量,建议在对应的类中实现,而不要放到MyNetwork中来。Person、Group类本身就保留许多自身的信息,可以直接实现对自己vauleSum等值的动态维护,MyNetwork只需要调用方法,具体实现就放在具体的位置最好。如果有需要的数据,完全可以在方法中传值,甚至使用单例模式直接访问MyNetwork的数据。
拓展
数据管理
首先我们规定各种Person各自管理的一些数据。
我们认为Producer与Customer无法建立直接的关系,只和Advertiser“打交道”,同理Customer也无法直接与Producer连接。
一个Producer只生产一种商品,但可以有多个Advertiser为他打广告;一个Advertiser只经营一种商品,并且向多个Customer推送,Customer根据preference选择从哪个Advertiser那里购买、购买哪种产品
public interface Advertiser extends Person {
/*@ public instance model int[] sales
@ public instance model Person[] customers
@ public instance model Person producer
@*/
}
public interface Producer extends Person {
/*@ public instance model Product product
@ public instance model Person[] advertisers
@ public instance model int[] sales
@ public instance model double ratio
@*/
}
public interface Customer extends Person {
/*@ public instance model int id;
@ public instance model Person[] advertisers
@ public instace model int[] bought
@ public instance model int perferece
@ public instance model Product
@*/
}
下面给出推送广告、购买产品和计算提成三个方法的JML规格
计算提成
/*@ public normal_behavior
@ requires containsProducer(id1);
@ ensures \results = (\sum int i; 0<=i && i<getProducers(id1).sales.length;
(getProducers(id1).sales[i]*getProducers(id1).product.price*getProducers(id1).ratio)/1);
@ public exceptional_behavior
@ signals (CustomerIdNotFoundeption e) !containsProducer(id1);
@*/
public int pushMoney(int id1)
推送广告
/*@ public normal_behavior
@ requires containsCustomer(id1) && containsAdvertiser(id2)
@ && getCustomer.containsAdvertiser(id2);
@ assignable getCustomer(id1).bought, getCustomer(id1).advertisers, getAdvertisers(id2).customers,
@ getAdvertisers(id2).sales;
@ ensures getCustomer(id1).bought.length = \old(getCustomer(id1).bought.length) + 1;
@ ensures getCustomer(id1).advertisers.length = \old(getCustomer(id1).advertisers.length) + 1;
@ ensures (\forall int i; 0<=i && i<getCustomer(id1).advertisers.length;
@ getCustomer(id1).advertisers[i] == \old(getCustomer(id1).advertisers[i]));
@ ensures getCustomer(id1).advertisers[\old(getCustomer(id1).advertisers.lenght)] == getAdvertiser(id2);
@ ensures (\forall int i; 0<=i && i<getCustomer(id1).bought.length;
@ getCustomer(id1).bought[i] == \old(getCustomer(id1).bought[i]));
@ ensures getCustomer(id1).bought[\old(getCustomer(id1).bought.length)] == 0;
@ ensures getAdvertiser(id2).sales.length = \old(getAdvertiser(id2).sales.length) + 1;
@ ensures getAdvertiser(id2).customers.length = \old(getAdvertiser(id2).customers.length) + 1;
@ ensures (\forall int i; 0<=i && i<getAdvertiser(id2).customers.length;
@ getAdvertiser(id2).customers[i] == \old(getAdvertiser(id2).customers[i]));
@ ensures getAdvertiser(id2).customers[\old(getAdvertiser(id2).customers.lenght)] == getCustomer(id1);
@ ensures (\forall int i; 0<=i && i<getAdvertiser(id2).sales.length;
@ getAdvertiser(id2).sales[i] == \old(getAdvertiser(id2).sales[i]));
@ ensures getAdvertiser(id2).sales[\old(getAdvertiser(id2).sales.length)] == 0;
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundeption e) !containsCustomer(id1);
@ signals (AdvertiserIIdNotFoundException e) containsCustomer(id1) && !containsAdvertiser(id2);
@ signals (AdvertisingDuplicateException e) containsCustomer(id1) && containsAdvertiser(id2)
@ && getCustomer(id1).containsAdvertiser(id2);
@*/
public void pushingMessage(int id1, int id2);
购买商品
/*@ public normal_behavior
@ requires containsCustomer(id1) && containsProduct(id3) && containsAdvertiser(id2)
&& getCustome(id1).containsAdvertiser(id2) && getProducer(id3).containsAdvertiser(id2);
@ assignable getProducer(id2).sales, getCustomer(id1).bought, getAdvertiser(id2).sales;
@ ensures (\forall int i; 0<=i && i < getCustomer(id1).bought.length
@ && getAdvertiser(id2) == \old(getCustomer(id1).advertisers[i]);
@ getCustomer(id1).bought[i] = \old(getCustomer(id1).bought[i]) + 1);
@ ensures (\forall int i; 0<=i && i < getCustomer(id1).brought.length
@ && getAdvertiser(id2) != \old(getCustomer(id1).advertiser[i]);
@ getCustomer(id1).bought[i] = \old(getCustomer(id1).bought[i]));
@ ensures (\forall int i; 0<=i && i < getAdvertiser(id2).sales.length
@ && getCustomer(id1) == \old(getAdvertiser(id2).customers[i]);
@ getAdvertiser(id2).sales[i] = \old(getAdvertiser(id2).sales[i]) + 1);
@ ensures (\forall int i; 0<=i && i < getAdvertiser(id2).sales.length
@ && getCustomer(id1) != \old(getAdvertiser(id2).customers[i]);
@ getAdvertiser(id2).sales[i] = \old(getAdvertiser(id2).sales[i]));
@ ensures (\forall int i; 0<=i && i < getProducer(id3).sales.length
@ && getAdvertiser(id2) == \old(getProducer(id3).advertiser[i]);
@ getProducer(id3).sales[i] = \old(getProducer(id3).sales[i]) + 1);
@ ensures (\forall int i; 0<=i && i < getProducer(id3).sales.length
@ && getAdvertiser(id2) != \old(getProducer(id3).advertiser[i]);
@ getProducer(id3).sales[i] = \old(getProducer(id3).sales[i]));
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundeption e) !containsCustomer(id1);
@ signals (AdvertiserIIdNotFoundException e) containsCustomer(id1) && !containsAdvertiser(id2);
@ signals (ProducerIdNotFoundException e) containsCustomer(id1) && containsAdvertiser(id2)
@ && !containsProducer(id3);
@ signals (NoAdvertisingException e) containsCustomer(id1) && containsAdvertiser(id2)
@ && containsProducer(id3)
@ && !getCustomer(id1).containsAdvertiser(id2);
@ signals (NoEmpolyingException e) containsCustomer(id1) && containsAdvertiser(id2)
@ && containsProducer(id3) && getCustomer(id1).containsAdvertiser(id2)
@ && !getProducer(id3).containsAdvertiser(id2);
@*/
public void buy(int id1, int id2, int id3);
心得体悟
在JML单元的学习中,我了解到了接口规格语言的魅力。相比可能产生的歧义 的自然语言,JML利用简单的规则,建构出了一套语言体系用以描述接口。在对方法的描述中,除了返回值,或者说除了要做什么 对JML这样一种逻辑严谨的语言来说,更重要的是不能做什么,或者说应当保留什么。而自然语言容易出现歧义,或者说容易出现漏洞的核心也总是在后者,例如老师上课举例所说的“给数组排序”这一操作,在这一要求下,直接清空数组似乎也完成了“排序”这一目标,即最后的数组是有序的。
但同样的,JML具有很高的复杂性,阅读JML远比阅读自然语言困难。从hw9开始,例如queryBlockSum这样的操作的JML,阅读起来不仅规模庞大而且难以理解,需要花费很多时间才能理解这几十行JML描述的只是个求联通分支数的操作。而且JML的描述仅仅是在功能上完成了规格设计,对接口的实现可以说没有太大的参考意义(从queryBlockSum中的遍历isCircle就可以看出)。从编写JML的一方而言,如何把自然语言描述下的功能转化为规范的JML也是一大难题。我们可以思考这样一个应用场景,功能设计者和程序员之间只靠“连通分支”四个字就能大致描述清楚的问题,如果使用JML的话,则需要双方都画上几十分钟思考如何转换这四个字和JML。我相信即使是对于JML熟练的程序员来说,这也是一件伤神的事情,更不要提对功能设计者了。
这点在我们周四的上机中也体现的非常明显。以往的上机大家都觉得时间富余,然而在第三单元的上机,无论是根据JML代码填空还是根据自然语言编写JML,都非常花时间,每次上机很多踩点提交。
在本单元的学习中,我认为自己还存在一个比较严重的问题:太过于将JML和JML的实现联系在一起。比如,到hw10时,我的代码里还存在相当多的ArrayList结构,这对一个频繁查找的图来说,是非常低效的。比如,在Exception的实现中,每个类的属性、构造函数和print()
方法都非常相似,但我仍然“按照"JML的描述单独实现了,并且为了偷懒复制粘贴的了许多部分,最后出了问题。对于一个面向对象的工程来说,继承方法、复用代码应该是非常核心的概念,而我却使用了复制粘贴的这样低级的操作。
总而言之,这个单元让我较为深入的了解了JML的优势与短处,也认识到了自己在面向对象编程中还存在的问题。