OO第三单元作业总结

一、JML理论基础

  JML是用于对Java程序进行规格化设计的一种表示语言,是对一种行为接口规格语言。通过它,可以完整的描述这个接口的行为,需要做到对于这个接口(方法)或者类型外部的内容的完整定义。

  JML有两种主要用法:1.开展规格化设计,这样的话对于代码实现人员来说是莫大的福利。2.针对已有的代码实现,书写对应的规格,从而提高代码的可维护性。

描述关键字含义
normal_behavior 正常行为
exceptional_behavior 异常行为
requires 前置条件
ensures 结束行为的后置条件
\result 方法执行后的返回值
\old(x) x在方法执行前的值
\forall 全程量词
\exists 存在量词
\sum 求和
\max 求极大值
\min 求极小值
assignable 可以改变的数据
&& 、||、==、==>、<==> 与、或、相等、蕴含、互蕴含
invariant 不变式,指在所有可见状态下,都要满足的性质
constraint 状态变化的约束
signals(e) 异常行为时,抛出异常e

  下面给出几个方法的JML规格及其实现。

 1 /*@ ensures \result == (people.length == 0? 0 : 
 2   @          ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
 3   @*/
 4 public /*@pure@*/ int getAgeMean();
 5  6 //这个getAgeMean方法要求我们对于people数据容器中的所有age求平均,然后将其返回。
 7  8  9 /*@ public normal_behavior
10       @ assignable \nothing;
11       @ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length; 
12       @                     acquaintance[i].getId() == person.getId()) || person.getId() == id;
13       @*/
14     public /*@pure@*/ boolean isLinked(Person person);
15 16 //这个isLinked方法,需要我们判断参数person.getId是否与自身id相等,或与acquaintance中的id相等,最后将比较结果返回
17 18 /*@ public normal_behavior
19       @ requires !(\exists int i; 0 <= i && i < people.length; people[i].equals(person));
20       @ assignable people, money;
21       @ ensures people.length == \old(people.length) + 1;
22       @ ensures money.length == people.length;
23       @ ensures (\forall int i; 0 <= i && i < \old(people.length); 
24       @         (\exists int j; 0 <= j && j < people.length; 
25       @         people[j] == \old(people[i]) && money[j] == \old(money[i])));
26       @ ensures (\exists int i; 0 <= i && i < people.length; people[i] == person && money[i] == 0);
27       @ also
28       @ public exceptional_behavior
29       @ signals (EqualPersonIdException e) (\exists int i; 0 <= i && i < people.length; 
30       @                                     people[i].equals(person));
31       @*/
32     public void addPerson(Person person) throws EqualPersonIdException;
33 34 //这个addPerson方法,需要我们做几件事,首先判断是否在people中已经存在参数person,若存在则抛出异常EqualPersonIdException,如果不存在,则需要满足以上几点:
35 //1.people数组size比原来大1 2.money数组与更新后people数组size相等 3.原来在people和money里面的元素在新的people和money数组中都必须能找得到
36 //4.新的元素也能在这两个数组更新后被找到。(简单的来说就是把person加到数组中)

 

  我们可以看到,其实JML规格理解起来并不难,就是用离散数学的方法严谨地表达一个接口的行为,只要有离散数学的一部分知识,熟悉语法之后读起来就不难。

 

二、SMT Solver的部署与验证

 

  我使用的是z3,但是z3并不能很好的支持太过于复杂的JML逻辑检查,所以我写了两个较为简单的方法进行测试。

 

 1 public class TestSMT {
 2     public static void main(String[] args) {
 3         System.out.println(Test.add(250, 520));
 4         System.out.println(Test.div(1, 2));
 5     }
 6     
 7     //@ensures \result == a + b;
 8     public int add(int a, int b) {
 9         return a + b;
10     }
11 12     //@ensures \result == a % b;
13     public int mod(int a, int b) {
14         return a % b;
15     }
16 }
17

 

  通过测试我们可以得到,一条INVALID信息,他告诉我们mod存在不完整的地方,因为除法取余会出现除0的情况。

 

三、JMLUnitNG的部署与测试

 

  JMLUnitNG同样不支持拥有全称量词和存在量词的关键词,所以本次测试我只选取了作业中两个很简单的方法。

 1 package demo;
 2 import java.util.ArrayList;
 3 public class TestJML {
 4     public ArrayList<Integer> peopleId = new ArrayList<Integer>();
 5     public static void main(String[] args) {
 6          TestJML test = new TestJML();
 7          test.peopleId.add(1);
 8          test.peopleId.add(2);
 9          int a = test.getSize();
10          System.out.println(a);
11          int b = test.getPersonId(1);
12          System.out.println(b);
13     }
14     // @ public normal_behaviour
15     // @ requires index >= 0 && index < size();
16     // @ assignable \nothing;
17     // @ ensures \result == peopleId.get(index);
18     public   /*@pure@*/ int getPersonId(int id) {
19         if (id < 0 || id >= getSize()) {
20             return -1;
21          }
22          return peopleId.get(id);
23     }
24     // @ public normal_behaviour
25     // @ ensures \result = peopleId.size();
26     public /*@pure@*/ int getSize(){
27         return peopleId.size();
28     }
29 }

 

  在自动生成测试文件之后,运行可以得到结果。

 

四、程序分析与设计策略

第一次作业

  第一次作业可以说只要读懂了JML规格就基本上能写对。只有一个方法需要提一下,就是isCircle方法。这个方法是判断在图里的两个人之间是否存在通路。我们可以使用的方法很多,比如dfs,bfs还有并查集。第一次作业中,我是用的是bfs广度搜索。

第二次作业

  第二次作业增加了组(多人组成)的数据查询,比如查询组内年龄平均,查询组内年龄方差,查询组内合集Value值等等。

  这些查询按照规格来写的话其实非常简单,就是一个循环或者二重循环就能解决。但是考虑到数据量和CPU时间的限制,我们应该维护好我们已经算出来的这些量,或者是说,我们一开始就要计算,每增加一个人就更新一次,或者是每需要访问一次,就更新一次,不需要访问的时候就将没有加入到更新的people单独记录下来。这样大大降低了所需的时间。

第三次作业

  本次作业的难点我认为在于两个方法:minpath和stronglinked,需要注意的还有blockSum。

  对于blockSum方法,我们通过阅读JML规格不难发现,这是查找图中连通块的个数。由于我在第三次作业中将isCircle改为了并查集,所以处理这个方法十分的方便。

  对于minPath方法,我们可以直接使用迪杰斯特拉求最短路经算法,但是要注意的是,由于数据量较为庞大,如果不做优化,每一次都需要在结构中寻找最短的那条路来更新的话,时间上没有保证。所以我采用了优先队列PriorityQueue的方式,或者说是堆优化的方式来处理,这样就不用每次遍历寻找最小值了。事实也证明,使用不优化的dij会导致两到三个点超时。

  对于Stronglinked方法,这里分析两种算法。

  首先是一个较为简单,容易理解的算法。我们知道如果两个点点双连通,那么至少就会有两条路连通这两个点,并且这两条路互相都没有交叉(也就是没有除起点终点外的相同的点)。那我们首先判断这两个点是否连通(这一步可以使用iscircle或者并查集)。第二步,找割点。通俗来说就是依次去掉路径上的点,这两个点还有没有连通。如果存在一个点,当去掉它时,两个点不连通了,说明这个点是路径必经的点(也就是不管有多少条路,都回到这个点)。说明点之间并不点双连通。那么如果不存在这样的点,那么点双连通。这里找割点的方法其实可以暴力一点,直接枚举其他的点,再删除他们,查看两点是否连通。要注意的是,并查集不支持删除点之后的查询,所以建议删除点之后用dfs或bfs检查连通性。这个方法思路较为简单也很好理解,但是缺点就在于他的时间复杂度比较高,大概是O(n^2)级别的。

  第二个方法就是tarjan算法。这个算法我并不会具体阐述怎么实现,网上有很多讲得不错的资料。大致思路就是,我从一个点出发,一直走,发现了一个强连通分量(这里用有向图的概念,无向图也可以类比),就把所有涉及到这个强连通分量的点都记录下来。那么我们使用tarjan算法,就可以求到这个图的所有最大强连通分量。我们stronglinked函数需要的更加简单,就是每次找出一个极大强连通分量时,判断两点是否在其中,如果在,那么点双连通。如果不存在这样的强连通分量,那么点双连通就不存在。

 

五、bug分析和修复情况

 

  本次作业我在第二次和第三次都发现了自己的bug。

  第二次作业我栽的十分惨烈,原因是我在维护ageMean的时候,现将ageMean求出,等到再加一个人的时候就乘以人数,补充新增的age,再除以人数+1。这样sb的公式我居然没有半点怀疑他是错的,因为这里使用的都是取整,所以误差会因为一次次增加age而增大,导致最后ageMean不准确。我其实这次作业跟同学对过拍,可惜的是我们两个人犯了同样的错误,这也给我一个很大的教训。

  这个bug的解决方式也很简单,直接增加一个agesum,每次需要ageMean的时候除以人数。

  第三次作业的bug十分明显,但还好,测评数据没有给我挖坑,不然我的强测又无了。在判断stronglinked的时候,存在一种情况,如果求出来的强连通分量只存在起点和终点时,这种情况是不算stronglinked的,但是我算进去了。

这个bug的解决方式仍然十分简单,只需要多判断一个点数大于2即可。

 

六、心得体会

 

  总的来说,这次作业难度不算大,因为规格写的非常详细,需要考虑的就是怎样完整而严谨的实现规格并进行合理地优化。但是我在第一点就做的不够好,测试不够到位导致我出现了这两个小bug,其中一个还让我直接没有进room。但是这个单元也让我学到了很多,尤其是在复习离散数学这方面,我觉得我又找回到了学离散数学的感觉。

 

posted @ 2020-05-20 22:46  力理利丽黎  阅读(177)  评论(0编辑  收藏  举报