OO第三单无总结
JML简介
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
表达式
原子表达式
\result表达式:表示一个非void
类型的方法执行所获得的结果,即方法执行后的返回值。
\old(expr
)表达式:用来表示一个表达式expr
在相应方法执行前的取值。
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
\nonnullelements(container
)表达式:表示container
对象中存储的对象不会有null
。
\type(type)表达式:返回类型type对应的类型(Class)。
\typeof(expr)表达式:该表达式返回expr对应的准确类型。
量化表达式
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式:返回给定范围内的表达式的和。
\product表达式:返回给定范围内的表达式的连乘结果。
\max表达式:返回给定范围内的表达式的最大值。
\min表达式:返回给定范围内的表达式的最小值。
\num_of表达式:返回指定变量中满足相应条件的取值个数。
集合表达式
可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
操作符
子类型关系操作符:E1<:E2
,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真。
等价关系操作符:b_expr1<==>b_expr2
或者b_expr1<=!=>b_expr2
,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是b_expr1==b_expr2
或者b_expr1!=b_expr2
。
推理操作符:b_expr1==>b_expr2
或者b_expr2<==b_expr1
。对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为true
。
变量引用操作符:\nothing指示一个空集;\everything指示一个全集。
方法规格
前置条件:通过requires子句来表示:requires P;
。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
后置条件:通过ensures子句来表示:ensures P;
。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。
副作用范围限定:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词assignable
或者modifiable
。
signals子句:结构为signals (***Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e
。
类型规格
不变式(invariant):要求在所有可见状态下都必须满足的特性,语法上定义invariant P
,其中invariant
为关键词,P
为谓词。
状态变化约束(constraint):对前序可见状态和当前可见状态的关系进行约束。
JML工具链
OpenJML:静态检查JML语法的正确性
JMLunitNG:针对JML规格,自动生成测试数据
SMT Solver部署
首先对Person.java进行验证,没有输出,表明Person.java中的规格没有问题
D:\Codes\OO\openjml>java -jar openjml.jar -exec Solvers-windows\z3-4.7.1.exe -esc com\oocourse\spec3\main\Person.java
D:\Codes\OO\openjml>
接下来对整个项目进行验证,首先将所有文件写到一个文件中,然后将这个文件的内容作为参数使用Solver进行验证。
dir *.java /s /b > list.txt
java -jar openjml.jar -exec Solvers-windows\z3-4.7.1.exe -esc @list.txt
验证结果如下,虽然出错了,但是其实出错的原因无关紧要。笔者查阅资料,发现原因是JML不支持三目运算符。
D:\Codes\OO\openjml\com\oocourse\spec3\main\Group.java:58: 错误: 不可比较的类型: int和INT#1
/*@ ensures \result == (people.length == 0? 0 :
^
其中, INT#1是交叉类型:
INT#1扩展Number,Comparable
D:\Codes\OO\openjml\com\oocourse\spec3\main\Group.java:63: 错误: 不可比较的类型: int和INT#1
/*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length;
^
其中, INT#1是交叉类型:
INT#1扩展Number,Comparable
2 个错误
将Group.java中的对应代码修改如下即可修复此问题。
/*@ public normal_behavior
@ requires people.length == 0;
@ ensures \result == 0;
@ also
@ public normal_behavior
@ requires people.length > 0;
@ ensures \result == (\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length;
@*/
public /*@pure@*/ int getAgeMean();
/*@ public normal_behavior
@ requires people.length == 0;
@ ensures \result == 0;
@ also
@ public normal_behavior
@ requires people.length > 0;
@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) /
@ people.length;
@*/
public /*@pure@*/ int getAgeVar();
JMLUnitNG部署
接下来对MyGroup类进行测试,依次输入如下三条命令。
java -jar jmlunitng.jar MyGroup.java
javac -cp jmlunitng.jar *.java
java -cp jmlunitng.jar MyGroup_JML_Test
得到结果如下:
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Passed: <>.addPerson(null)
Passed: <>.addPerson(null)
Passed: <>.addPerson(null)
Passed: <>.delPerson(null)
Passed: <>.delPerson(null)
Passed: <>.delPerson(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(java.lang.Object@1517365b)
Passed: <>.equals(java.lang.Object@44e81672)
Passed: <>.equals(java.lang.Object@4ca8195f)
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getSize()
Passed: <>.getSize()
Passed: <>.getSize()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.updateRelation(null, null)
Passed: <>.updateRelation(null, null)
Passed: <>.updateRelation(null, null)
===============================================
Command line suite
Total tests run: 43, Failures: 1, Skips: 0
===============================================
唯一的一个失败属于无关紧要的小问题。但是可以看出JMLUnitNG更多是对int的边界、null等极端数据进行测试,对于函数本身的正确性价值不大,更多是检验了代码的鲁棒性。
架构设计分析
第一次作业
对于MyPerson类,笔者使用双ArrayList结构保存关于熟人的信息,即一个ArrayList保存熟人,另一个保存对应的value。
在MyNetwork中,使用HashMap保存网络中的人,其中key为人的id,方便直接通过id得到对应的人。对于最复杂的方法isCircle,笔者考虑到未来可能会扩展出对网络中的成员进行删除的操作,因此采用bfs的方法进行判断。
第二次作业
考虑到在ArrayList中查找是否有某个元素需要遍历,复杂度较高,因此MyPerson中改为使用HashMap保存熟人,其中key为熟人id,value为由熟人和熟人的value组成的Pair。
MyNetwork中保存人的方式不变,对组的保存与对人的保存类似,均为HashMap且key为id。
新增了MyGroup类和大量与此有关的方法。MyGroup中同样使用HashMap保存组中的人,并且记录了组中现有的人的relationSum,valueSum,conflictSum,ageSum,ageSquareSum,在每次向组中添加人的时候会更新这些变量。在MyNetwork中添加关系的时候也会遍历每个组,检查组中是否有这两人,如果有,就也更新一下上述这些变量。通过缓存法,各种有关组的查询操作的复杂度均降为$O(1)$。
第三次作业
MyPerson类相比上次不变。
MyNetwork中增加了保存各个人的钱数的HashMap,key为人的id。由于queryBlockSum需要大量调用isCircle方法,且网络中不存在删除成员的方法,因此将$O(n)$复杂度的bfs换成了$O(\alpha)$的带路径压缩的并查集。这一次两个较为复杂的方法是queryMinPath和queryStrongLink,笔者分别采用了堆优化的Dijkstra算法和求双联通分量的Tarjan算法进行处理。为了使类的职责更明确,笔者将两种算法分别写到了两个类中,当需要调用算法的时候只需要创建类的对象并将人员网络传入即可,从而为MyNetwork类减负。而为了更方便的对Dijkstra和Tarjan过程中的信息进行处理,笔者创建了DijkInfo和TarjanInfo类,用于记录和修改有关的数据(如距离、是否访问等)。
由于增加了删除组内成员的操作,因此MyGroup增加delPerson方法,具体效果大致为addPerson方法的逆操作,将各个缓存的变量进行更新。
bug情况
笔者对本单元的作业展开了较为充分的测试,因此笔者在强测、互测中均未出现bug。
笔者的测试的思想是数据生成+对拍,通过这样的测试在提交截止前便发现了所有的bug。
在三次互测中,笔者采用的是对拍为主、读代码为辅的测试方法。第一次作业未发现bug。第二次作业发现了别人的大量bug,包括getRelationSum
和getValueSum
的超时、某些方法未判断id1==id2、打错字等问题。第三次作业笔者发现一位同学使用朴素dfs找环的方法处理queryStrongLink,因此构造了一个具有42个点的完全图,理论复杂度为$O(42!)$,将这位同学的超时问题暴露无遗。
最后的一点感想
笔者认为本单元总体难度比较简单,思考量更多在算法上而不是架构上,实际上由于JML规格已经给出,接口也无法更改,因此架构上的发挥余地并不大。
总体而言,笔者对自己在这一单元的表现还是较为满意的,三次作业均没有出现问题,就是一种小小的成功。希望这种成功能够继续延续到下个单无元。笔者认为笔者本单元的良好发挥,最大的原因是吸取了第一单元的教训,每次都做了大量的测试,无论数据量还是数据规模都很庞大,从而在截止提交前就发现了所有bug。
同时,笔者在互测过程中也发现有不少人因为测试做的不充分而翻车,有的人是测试的数据组数太少,有人是测试的数据规模太小。比如第二单元一个房间中有3个人是随便生成一组数据都能出错的,另外还有两人把queryRelationSum和queryValueSum写成了$O(n^2)$复杂度,也很容易就被笔者构造的数据卡超时。吃别人的堑,长自己的智。可见,做测试依然是非常重要的一件事情,笔者在下一单元也将延续这一好习惯。
另一点教训就是对于实现的细节还是要注意一下,虽然笔者没有出这个问题,但是有不少人写好了Tarjan后以为万事大吉,万万没想到居然因为堆优化Dijkstra的实现不够好而超时翻车。这样的原因可能有很多,比如过度依赖HashMap,或是不了解ArrayList的contains方法的实现等。因此在以后使用容器的时候也需要小心谨慎,最好能了解容器的实现后再去用。
要说有什么不足的话,大概有两点。一是在MyNetwork中通过向下转型的方法调用自己给MyPerson增加的一些方法,破坏了LSP原则。二是笔者并没有使用JML工具链、Junit等工具来进行测试,而是依然在使用较为传统的对拍测试方法。虽然简单高效,可以在短时间内用大量数据进行检查,但是在实际工程中很难有代码用来对拍,更多的还是依赖于单元测试。