JML语言基础
1.1原子表达式
\result表示方法执行后的返回值
\old(expr)表示表达式expr在相应方法执行前的取值,涉及到对象时只能判断对象引用是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化
\not_assigned(x,y..)表示括号中变量是否在方法执行过程中被赋值,如果没有返回true
\not_modifyed(x,y...)表示括号中变量在方法执行过程中取值是否变化,如果没有返回true
\nonnullelements(constainer)表示对象中储存的对象不会有null
\type(type)返回type对应的类型(Class)
\typeof(expr)返回expr对应的准确类型
1.2量化表达式
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式:返回给定范围内的表达式的和。(\sum int i; 0 <= i && i < 5; i)
,这个表达式的意思计算[0,5)范围内的整数i的和,即0+1+2+3+4==10。注意中间的0 <= i && i < 5
是对i范围的限制,求和表达式为最后面的那个i
。
\product表达式:返回给定范围内的表达式的连乘结果。(\product int i; 0 < i && i < 5; i)
,这个表达式的意思是针对(0,5)范围的整数的连乘结果,即1* 2* 3 * 4。
\max表达式:返回给定范围内的表达式的最大值。(\max int i; 0 <= i && i < 5; i)
,这个表达式返回[0,5)中的最大的整数,即4。
\min表达式:返回给定范围内的表达式的最小值。(\min int i; 0 <= i && i < 5; i)
,这个表达式返回[0,5)中的最小的整数,即0。
\num_of表达式:返回指定变量中满足相应条件的取值个数。(\num_of int x; 0
1.3集合表达式
集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() }
表示构造一个JMLObjectSet对象,其中包含的元素类型为Integer,该集合中的所有元素都在容器集合s中出现
1.4操作符
(1) 子类型关系操作符:E1<:E2
,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真。任意一个类X,都必然满足X.TYPE<:Object.TYPE
。
(2) 等价关系操作符:b_expr1<==>b_expr2
或者b_expr1<=!=>b_expr2
,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是b_expr1==b_expr2
或者b_expr1!=b_expr2
。可以看出,这两个操作符和Java中的==
和!=
具有相同的效果,按照JML语言定义,<==>
比==
的优先级要低,同样<=!=>
比!=
的优先级低。
(3) 推理操作符:b_expr1==>b_expr2
或者b_expr2<==b_expr1
。对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为true
。
(4) 变量引用操作符:\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。
2方法规格
- 前置条件(pre-condition)
前置条件通过requires子句来表示:requires P;
。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
- 后置条件(post-condition)
后置条件通过ensures子句来表示:ensures P;
。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。
- 副作用范围限定(side-effects)
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词assignable
或者modifiable
。从语法上来看,副作用约束子句共有两种形态,一种不指明具体的变量,而是用JML关键词来概括;另一种则是指明具体的变量列表。
- signals子句
signals子句的结构为signals (***Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e
。
还有一个简化的signals子句,即signals_only子句,后面跟着一个异常类型。signals子句强调在对象状态满足某个条件时会抛出符合相应类型的异常;而signals_only则不强调对象状态条件,强调满足前置条件时抛出相应的异常。
3类型规格
- 不变式invariant
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义invariant P
,其中invariant
为关键词,P
为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。下面所述的几种时刻下对象o的状态都是可见状态:
-
对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
-
在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
-
在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
-
在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
-
在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
-
在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻
由上面的定义可知,凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态。这里的可见不是一般意义上的能否见到,而是带有完整可见的意思。在会修改状态的方法执行期间,对象状态不稳定,随时可能会被修改。换句话说,在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调在任意可见状态下都要满足不变式。
- 状态变化约束constraint
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。如下面的例子:
JML工具链情况
OpenJml:进行JML语法检查、代码静态检查
JMLUnitNG:针对类生成自动测试样例并进行测试
部署JMLUnitNG生成测试用例
这是利用JMLUnitNG对我自己写的MyGroup生成的测试用例
下面是测试用例的测试情况:
不难发现生成的测试数据主要是针对一些极端的输入情况,例如输入int的最大值最小值和null的Person等
而且还真的测出了我程序的漏洞,三个和Person输入相关的类都没有考虑到空指针输入的情况。
架构梳理
我就是老师所说的,完全按照给的JML写了三个大类的反面典型...
虽然一开始就意识到这个单元的作业和图紧密相关,但是一方面是图相关的知识学得不是很好,另一方面很多JML要求的方法所需要的数据都是紧密相关的。在我试图拆解方法和数据的时候,发现如果自己抽象的层次不好,不是白白增加了数据冗余,就是类之间需要传递大量的数据才能完成相同的工作。所以最后除了为了实现优先队列,抽象出了一个类之外,剩下完全就是JML要求的三个主类。
bug和修复情况
总体来说这个单元的作业情况还算理想,前两次作业都拿到了强测100分和互测没有被hack,只有第三次作业比较凄惨的在强测中TLE了四个点。
其实在强测截止前两天和室友对拍的时候我就发现了自己的程序运行时间过长的问题,但是经过了两天的不断修改,甚至和室友核对算法,我都没能找到导致运行时间过长的原因。最终只能抱着侥幸心理希望不被抓到,显然结果是行不通的。
强测结果出来之后我又花了两天时间寻找可能优化时间的地方,而成果也不过是擦着边儿修复了两个点。这让我百思不得其解,其他同学为什么就都能通过测试呢?
直到在水群里看到有同学提到HashMap的初始化问题,我才知道自己问题所在。
在写作业的时候我在网上查询了HashMap的用法,发现HashMap的初始容量只有16,有人说如果存入的数据超过容量的时候就需要动态扩容,重新构建映射关系,会耽误大量的时间。因此我在作业中使用的HashMap我都初始化了一个题目上限的值,这样就能避免动态扩容的发生。
修复bug之后,经过助教的分享我又发现有人测试之后发现设定初始值对HashMap性能不一定有正面的提升。查看了一下HashMap的代码还是有些摸不着头脑...因为这个原因花费了几天的时间debug实在让人沮丧。
心得体会
JML确实不是一个容易理解的东西,尤其是当它和离散数学的知识牵扯上关系的时候。多亏了助教在本单元作业中给的JML十分详细,我在完成作业时候才不至于纠结于到底如何实现。不过这也是一把双刃剑,使得我没能够跳出题目的框架自己选择更合适的抽象层次(虽然我觉得以我目前的OO理解可能自己乱抽象出来效果会更差)。
这个单元的作业我最大的感触就是JML的规范性和灵活性。一方面JML严格界定了每个类每个方法的责任,充分体现了契约式编程的思想,使得每个方法在单独测试的时候分工十分清晰,出现问题很容易追溯根源;另一方面JML并不要求具体实现,因而给效率更高的实现留下了自由发挥的空间。
我觉得课程组在这里的设置还是比较合理的,并没有要求特别高的效率,让大家都去实现很复杂的算法;而是需要在最简单粗暴的基础上加上一定的优化,既能让我们体会JML优越性也不至于让人望而却步。
不过我想课程组可能也没有想到这个HashMap的初始值居然卡掉了一批同学...我想以后在不了解底层原理的时候我还是应该少节外生枝,免得像这次一样画蛇添足反而把自己给坑了。