OO第三单元总结
OO第三单元总结
1. JML基础梳理及工具链
1.1 注释结构
- JML以javadoc注释的方式来表示规格,每行都以@起头
- 行注释的表示方式为
//@annotation
;块注释的表示方式为/* @ annotation @*/
1.2 JML表达式
JML相对于Java新增的表达式成分仅用于JML中的断言(assertion)语句和其他相关的注释体。
在JML断言中,不可以使用带有赋值语义的操作符,如++,--,+=
等操作符,因为这样的操作符会对被限制的相关变量的状态进行修改,产生副作用。
1.2.1 原子表达式
-
\result
表达式 表示一个非
void
类型的方法执行所获得的结果,即方法执行后的返回值。\result表达式的类型就是方法声明中定义的返回值类型。 -
\old(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。TYPE是JML采用的缩略表示,等同于Java中的java.lang.Class
。 -
\typeof(expr)
表达式 返回expr对应的准确类型。如\typeof(
false
)为Boolean.TYPE。
1.2.2 量化表达式
-
\forall
表达式 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
-
\exists
表达式 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
-
\sum
表达式 返回给定范围内的表达式的和。
-
\product
表达式 返回给定范围内的表达式的连乘结果。
-
\max
表达式 返回给定范围内的表达式的最大值。
-
\min
表达式 返回给定范围内的表达式的最小值。
-
\num_of
表达式 返回指定变量中满足相应条件的取值个数。
1.2.3 集合表达式
-
集合构造表达式
可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
集合构造表达式的一般形式为:
new ST {T x|R(x)&&P(x)}
,其中的R(x)对应集合中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
1.2.4 操作符
-
子类型关系操作符
E1<:E2
如果类型E1是类型E2的子类型或是相同类型,则该表达式的结果为真,否则为假。
-
等价关系操作符
b_expr1<==>b_expr2
或b_expr1<=!=>b_expr2
这两个意思是
b_expr1==b_expr2
或b_expr1!=b_expr2
,其中b_expr1和b_expr2都是布尔表达式。这两个操作符和Java中的==
和!=
具有相同的效果,但<==>
比==
的优先级要低,<=!=>
比!=
的优先级低。 -
推理操作符
b_expr1==>b_expr2
或b_expr2<==b_expr1
对于表达式
b_expr1==>b_expr2
而言,当b_expr1==false
或b_expr1==true
且b_expr2==true
时,整个表达式的值为true
。 -
变量引用操作符
比如
\nothing
指示一个空集;\everything
指示一个全集,即包括当前作用域下能够访问到的所有变量。
1.3 方法规格
1.3.1 前置条件
前置条件是对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
前置条件通过requires子句来表示:**requires P;**
。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。若方法规格中有多个requires子句,则调用者必须同时满足所有的并列子句要求。
1.3.2 后置条件
后置条件是对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。
后置条件通过ensures子句来表示:ensures P;
。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。若方法规格中有多个ensures子句,则方法实现者必须同时满足有所并列ensures子句的要求。
1.3.3 副作用范围限定
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
JML提供了副作用约束子句,使用关键词assignable
或者modifiable
。
用/*@ pure @ */
表示的方法都是纯粹查询方法,即方法的执行不会有任何副作用。
1.3.4 signals
子句
signals子句的结构为signals (***Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e
。如果一个方法在运行时会抛出异常,一定要在方法声明中明确指出(使用Java的throws
表达式),且必须确保signals子句中给出的异常类型一定等同于方法声明中给出的异常类型,或者是后者的子类型。
signals_only子句不强调对象状态条件,强调满足前置条件时抛出相应的异常。
为了更加明确的区分异常,会针对输入参数的取值范围抛出不同的异常,从而提醒调用者进行不同的处理。可以使用多个exceptional_behavior。
1.4 类型规格
1.4.1 不变式invariant
不变式是要求在所有可见状态下都必须满足的特性,语法上定义invariant P
,其中invariant
为关键词,P
为谓词。
下面所述的几种时刻下对象o的状态都是可见状态
- 对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
- 在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
- 在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
- 在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
- 在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
- 在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻
凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态。在方法执行期间,对象的不变式有可能不满足。
1.4.2 状态约束变化constraint
对象的状态在变化时往往也许满足一些约束。
1.5 应用工具链情况
- JML高亮插件:可以帮助更好地阅读JML规格
- OpenJML:可以根据JML对实现进行静态的检查
- JMLUnitNG:可以根据JML生成自动的测试样例,用于进行单元测试
2. 部署JMLUnitNG
由于在部署JMLUnitNG遇到了很多错误,所以将整个过程记录了下来,实测可行
- 首先需要下载并安装jdk8,具体操作可参考https://blog.csdn.net/weixin_42109012/article/details/94388518,在所有操作完成后,需要达到的效果是打开cmd,并且执行
java -version
和javac -version
指令,得到的是自己安装的版本。 - 之后的具体操作可以参考https://www.cnblogs.com/nbao01/p/12935615.html,需要注意的是其中的第一步操作,除了
package
需要调整外,import
的地方也需要进行相应的调整 - 本人的最后执行结果如下所示
在测试中一共运行了58个测试用例,其中失败了10个。它生成了三个MyGroup
对象,其id分别为-2147483648、0和2147483647。JMLUnitNG在测试时,如果参数是int类型,则会传入-2147483648、0和2147483647进行测试;如果参数是Object子类,则会传入null。可见,JMLUnitNG的测试注重于边界情况,但是addPerson
、delPerson
和hasPerson
方法传入的对象不能是null,所以才会失败。整体尝试发现,JMLUnitNG不怎么好用,还是自己写测试样例比较保险。
3. 架构设计
3.1 第一次作业
此次作业的架构比较简单,首先依据Person
接口和JML规格来实现MyPerson
,MyPerson
类生成的每个对象都会有id
(编号)、name
(姓名)、character
(性格)、age
(年龄)、acquaintAndVaule
(认识的Person
和两人关系的权重)和circleFlag
(所处的连通块编号)等属性,其中,因为id
是独一无二,可以作为equals
的判断依据;acquaintAndValue
用HashMap
容器实现,Person
作为key,两人关系的权重作为value。
然后依据Network
接口和JML规格来实现MyNetwork
,其中people
属性用来表示社交网络中的Person
,用HashMap
容器实现,id
作为key,Person
作为value。在判断两人是否间接认识即isCircle
时,采用的是并查集算法。每个人初始的circleFlag
是其id
,每次addRelation
时,都将其中一人的circleFlag
变成另一人的circleFlag
,基于QuickUnion实现并查集,并通过高度低的树向高度高的数合并和路径压缩,就可以高效查询网络中两个节点的连接状态。
3.2 第二次作业
这次作业的架构则是在上次作业的架构上作了扩展,首先依据Person
接口和JML规格来实现MyPerson
,和上次作业大致相同,其中MyPerson
类中的acquaintAndValue
拆分成了acquaintance
和value
,都用HashMap
容器实现,用id
作为key,用Person
和value
分别作为value。
然后依据Group
接口和JML规格来实现MyGroup
,MyGroup
类生成的每个对象都会有id
(群组编号)、people
(群组中的人)、peopleSize
(群组中的人员数)、ageSum
(群组内所有人的年龄之和)、conflictSum
(群组内所有人的异或和)、relationSum
(群组内关系的数量之和)和valueSum
(群组内关系的权重之和)等属性。其中,people
用HashMap
容器实现,id
作为key,Person
作为value。其中people
、peopleSize
、ageSum
和conflictSum
都是在addPerson
时动态维护的,relationSum
和valueSum
都是在addPerson
和addRelation
时动态维护的。
最后依据Network
接口和JML规格来实现MyNetwork
,在上次作业的基础上增加了groups
属性,用来记录Network
中所有的群组,用HashMap
来实现,群组编号id
作为key,群组Group
作为value。
3.3 第三次作业
这次作业则是在上次作业的基础上做了进一步的扩展。其中,MyGroup
类中增加delPerson
方法,在执行该方法时,要动态维护people
、peopleSize
、ageSum
和conflictSum
等属性。
MyNetwork
相较于上次作业增加了很多新方法,为了更好地实现,增加了Relation
类,记录一个Relation
中的两个人的编号和关系权重。其中,queryMinPath
要求两人间最短社交路径,本次作业使用了Dijkstra算法来实现;queryStrongLinked
要求两人间是否有两条不相交的社交路径,本次作业使用了tarjan算法来寻找点双连通分量;queryBlockSum
要求连通块数目,由于使用了并查集,所以不同的circleFlag
数量就是连通块数目。
4. Bug修复情况
本单元只在第三次作业中出现3个点CTLE的情况,这三个点的特征是大量的queryMinPath的命令。在求最短路径时,本人采用的是Dijkstra算法,所以重新提交后,bug修复。
5. 心得体会
第三单元的作业是JML规格下进行代码的实现,在具体实现时,数据的实现方式、容器的选择、方法的实现算法都是值得仔细考察的内容。JML规格像是签订了一份“契约”,在未来的工程实践中也会十分有用。经过整个单元的学习,我对JML规格有了更深的认识,但是在工具链的使用和单元测试等方面还需要进一步的了解和学习。