一、JML的理论基础与应用工具链:
1、理论基础:
\result表达式:表示一个非`void`类型的方法执行所获得的结果,即方法执行后的返回值。形式如:"\result == xxx",并保证该结果为True。
\old(`expr`)表达式:用来表示一个表达式`expr`在相应方法执行前的取值。一般用来表示方法执行前后'expr'的变化情况。
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。未被赋值则返回True,否则false。
\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
\nonnullelements(`container`)表达式:表示`container`对象中存储的对象不会有`null`。
\type(type)表达式:返回类型type对应的类型(Class),如type(`boolean`)为Boolean.TYPE。
\typeof(expr)表达式:该表达式返回expr对应的准确类型。
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。形式如:"(\forall 变量;限定条件;满足条件)"。限定条件约束变量的范围,满足条件则必须在实现规格时保证其结果为True。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。用法同\forall。
\sum表达式:返回给定范围内的表达式的和。形式如:"(\sum 变量;限定条件;变量)"。两个变量可相同,可不相同。
\product表达式:返回给定范围内的表达式的连乘结果。用法如上。
\max表达式:返回给定范围内的表达式的最大值。用法如上。
\min表达式:返回给定范围内的表达式的最小值。用法如上。
\num_of表达式:返回指定变量中满足相应条件的取值个数。形式如:"(\num_of 变量;限定条件;限定条件)"。
requires xxx:"xxx"为前置条件。
assignable xxx:"xxx"为影响范围。
ensures xxx:"xxx"为后置条件。
2、工具链:
OpenJML,JMLUnit,STM Solver,ESC/Java2,Daikon,KeY,Krakatoa,JMLEclipse,Sireum/Kiasan,TACO,VerCors verifier。目前只使用过JMLUnit。
二、Group测试用例分析:
junit好像没办法自动生成测试样例(或许是还没找到那个功能),所以一直是手动编写测试样例。先说一下测试基本思路:
/*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length; @ (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) / @ people.length)); @*/ public /*@pure@*/ int getAgeVar();
针对这类只有后置条件的代码,在相应的测试代码中建立group,随机向其中添加数据后,用assertequal与在测试中搭建的求方差公式多次测试即可。
/*@ public normal_behavior @ requires people.length > 0; @ ensures (\exists BigInteger[] temp; @ temp.length == people.length && temp[0] == people[0].getCharacter(); @ (\forall int i; 1 <= i && i < temp.length; @ temp[i] == temp[i-1].xor(people[i].getCharacter())) && @ \result == temp[temp.length - 1]); @ also @ public normal_behavior @ requires people.length == 0; @ ensures \result == BigInteger.ZERO; @*/ public /*@pure@*/ BigInteger getConflictSum();
针对这类有多种行为的代码,针对每一种行为都应当充分测试,对于该份代码,第二种情况,较为简单,只需在group未加入person时,对其反复测试即可。而第一种情况,需要保证其搭建temp的正确性,可先检查返回值的正确性,再反向推导,确定temp[0]的正确性。
public void addPerson(Person person);
针对这类没有规格的代码,应该由我们自己根据自己搭建的group结构去设计,对该方法,我的规格如下:
/*@ensures \old(people.length) + 1 == people.length; @ensures isupdaters == true; @ensures isupdatevs == true; @ensures isupdatecs == true; @ensures isupdateam == true; @ensures isupdateav == true; @ensures person.getgroupsid == \old(person.getgroupsid + 1); @*/
三、架构设计梳理:
第一次作业完全按照jml规格实现,并无太多自己的设计。
第二次作业在group中加入了relationsum,valuesum,conflictsum,agemean,agevar分别记录对应方法运行后所返回的数据,以及isupdaters,isupdatevs,isupdatecs,isupdateam,isupdateav分别用来表示对应数据是否需要更新。该设计可以在查询较为频繁的时候,减少一定量的计算时间。
第三次作业,较前两次有了较多的架构设计:
在Mynetwork中,添加了father和child两个hashmap变量,以便建立一个两层的联通图,该连通图可简化iscircle,queryStronglink,queryBlocksum,queryMinPath的操作,但其对于queryMinPath的简化还不够彻底,导致仍然超时。而intodper变量,则用于记录network中person的添加顺序。
四、bug以及修复情况:
第一单元,用Arrylist实现,无bug。
第二单元,用Arrylist实现,80%超时。仔细思考后发现,Arrylist在查询和添加操作较多时,效率过低,换用Hashmap,成功通过。
第三单元,queryblocksum,理解错误,导致出现Bug,以及stronglink和querryminpsth的算法未进行优化,出现CTLE。还有容器的操作错误,当容器中储存的是Interger型数据时,若想删除对应数据,则应该将int数据转为Number型,若直接删除int,则其会删除对应下标的数据。关于优先队列,仍旧犯了不小的错误,Java中的PriorityQueue,只有在添加时,才对该数据进行排序,而修改其中元素数据内容不会触发排序(就是这个地方,一直想不到办法来优化其复杂度)。加入联通图结构,简化复杂算法。但对于querryminpath的简化仍然不够,还在继续改进中。
五、心得体会:
必须理解,jml规格描述,只是告诉实现者,其中各个方法和类,应该满足的条件。然而在实现的时候,设计者不能只实现jml的描述,他应该明白jml所描述的功能只是实现目标的子集,还必须对类作优化,以便可以用较少的资源去处理数据。