JML是用于对Java程
在这里插入代码片
序进行规格化设计的一种表示语言。通过JML及其支持工具,不仅可以基于规格自动测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。JML的两种用法:
开展规格化设计和
提升代码可维护性。
注释结构
JML以javadoc注释的方式表示规格,每行以@起头。注释方式有两种:行注释//@ annotation
和块注释/*@ annotation @*/
。注释一般放在被注释成分的临近上部。
package org.jmlspecs.samples.jmlrefman; // line 1
// line 2
public abstract class IntHeap { // line 3
// line 4
//@ public model non_null int [] elements; // line 5
// line 6
/*@ public normal_behavior // line 7
@ requires elements.length >= 1; // line 8
@ assignable \nothing; // line 9
@ ensures \result // line 10
@ == (\max int j; // line 11
@ 0 <= j && j < elements.length; // line 12
@ elements[j]); // line 13
@*/ // line 14
public abstract /*@ pure @*/ int largest(); // line 15
// line 16
//@ ensures \result == elements.length; // line 17
public abstract /*@ pure @*/ int size(); // line 18
}; // line 19
第1、3、15、8、19行为有效的代码。抽象类IntHeap中包含largest()和size()两个抽象方法。
第15、18行:/*@ pure @*/
表示为纯粹查询方法(不会有任何副作用)。
第5行:IntHeap所管理的数据规格。int[] elements是规格层次描述,不一定需要有一模一样的属性定义。non_null表示elements数组对象引用不能为null。如果是静态规格变量,声明为//@ public static model non_null int[] elements
;实例规格变量则为//@ public instance model non_null int[] elements
第17行:size方法后置条件(postcondition)。\result为JML关键字,表示返回结果。该句表示任何时候执行方法都返回elements.length。
第7-14行:该段内容表示largest的规格,包括三个部分:
(1) requires定义前置条件
(2) assignable副作用范围限定
\nothing关键字表示不对任何属性进行修改(pure方法)
(3) ensures定义后置条件
返回elements中最大的那个
JML表达式
注意:JML断言中不可以使用带有赋值语句的操作符
原子表达式
- \result : 非void型方法执行结果。
- \old(expr) : 表示表达式expr在相应方法执行前的取值。
该表达式涉及到评估 expr 中的对象是否发生变化,但是针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。 - \not_assigned(x,y,…) : 用来表示括号中的变量是否在方法执行过程中被赋值。
该表达式主要用于后置条件的约束表示上(限制一个方法的实现不能对列表中的变量进行赋值)。 - \not_modifed(x,y,…) : 表达式限制括号中的变量在方法执行期间的取值未发生变化。
- \nonnullelements(container) : 表示 container 对象中存储的对象不会有 null。等价于:
container != null &&
(\forall int i; 0 <= i && i < container.length; \\ forall为针对所有i
container[i] != null)
- \type(type) : 返回类型type对应的类型(Class)。
e.g. type( boolean )为Boolean.TYPE
- \typeof(expr) : 该表达式返回expr对应的准确类型。
e.g. \typeof( false )
量化表达式
- \forall : 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
(\forall inti,j; 0 <= i && i < j && j < 10; a[i] < a[j])
// 如果表达式为真,则说明 a是升序数组
- \exists : 存在量词修饰的表达式。
(\existsint i; 0 <= i && i < 10; a[i] < 0)
// 针对 0 <= i < 10,存在 a[i]小于0
- \sum : 返回给定范围内的表达式的和。
(\sum int i; 0 <= i && i < 5; i)
// 计算 0 + 1 + 2 + 3 + 4
- \product : 返回给定范围内的表达式的连乘结果。
(\product int i; 0 < i && i < 5; i)
// 计算 1 * 2 * 3 * 4
- \max : 返回给定范围内的表达式的最大值。
(\max int i; 0 <= i && i < 5; i)
- \min : 返回给定范围内的表达式的最小值。
(\min int i; 0 <= i && i < 5; i)
- \num_of : 返回指定变量中满足相应条件的取值个数。
(\num_of int x; 0<x && x<=20;x%2==0)
(\num_of T x;R(x);P(x)) == (\sum T x;R(x)&&P(x);1)
// 0 到 20中能被 2整除的数的个数,得 10
集合表达式
可以构造一个局部的集合(容器),明确集合中可以包含的元素。
new ST {T x|R(x)&&P(x)}
// 其中的 R(x)对应集合中 x的范围,通常是来自于某个既有集合中的元素,如 s.has(x)
// P(x)对应 x取值的约束
new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() }
// 表示构造一个 JMLObjectSet对象,其中包含的元素类型为 Integer
// 该集合中的所有元素都在容器集合s中出现(注:该容器集合指 Java程序中构建的容器,比如 ArrayList),且整数值大于0。
操作符
- 子类型关系操作符
E1<:E2
// 如果 E1是 E2的子类型或者与 E2类型相同,则返回真
// 注意任何类型都是 Object类型的子类型
- 等价关系操作符
b_expr1<==>b_expr2
b_expr1<=!=>b_expr2
// <==> 比 == 的优先级低
// <=!=> 比 != 的优先级低
- 推理操作符
b_expr1==>b_expr2
// 当 b_expr1==false || b_expr1==true || b_expr2==true 时,为 true
b_expr2<==b_expr1
- 变量引用操作符
可以引用Java代码;JML中的变量;\nothing(空集);\everything(全集,当前作用域下能访问到的所有变量)
assignable \nothing
// 当前作用域下每个变量都不可以在方法执行过程中被赋值
方法规格
方法规格的核心内容包括三个方面:前置条件、后置条件和副作用约定。
- 前置条件 : 对方法输入参数的限制,不满足则不能保证正确性。
requires P;
// 方法实现者确保方法执行返回结果一定满足谓词 P的要求,即确保 P为真
public /*@ pure @*/ String getName(); // pure表示不会改变对象
//@ ensures \result == bachelor || \result == master;
public /*@ pure @*/ int getStatus();
//@ ensures \result >= 0;
public /*@ pure @*/ int getCredits();
// 有些前置条件可以引用 pure方法返回的结果
/*@ requires c >= 0;
@ ensures getCredits() == \old(getCredits()) + c;
@*/
public void addCredits(int c);
- 后置条件 : 对方法执行结果的限制,执行后如果满足则执行正确。
ensures P;
// 在前置或者后置条件中一个容器中所有变量约束
/*@ requires size < limit && !contains(elem);
@ ensures \result == true;
@ ensures contains(elem);
@ ensures (\forall int e;
@ e != elem;
@ contains(e) <==> \old(contains(e)));
@ ensures size == \old(size) + 1;
@*/
public boolean add(int elem) {/*...*/}
/*@ ensures !contains(elem);
@ ensures (\forall int e;
@ e != elem;
@ contains(e) <==> \old(contains(e)));
@ ensures \old(contains(elem)) ==> size == \old(size) - 1;
@ ensures !\old(contains(elem)) ==> size == \old(size);
@*/
public void remove(int elem) {/*...*/}
// 如果输入参数在容器中,则移除该整数;不能移除容器中不等于输入参数的任何整数
- 副作用约定 : 指方法在执行过程中对输入对象或 this 对象进行了修改(对
其成员变量进行了赋值,或者调用其修改方法)。
方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。、
注1:不允许在副作用约束子句中指定规格声明的变量数据。
注2:private量通常调用者不可见。可以用/*@ spec_public @*/
注释一个类的私有成员变量,表示在规格中可以直接使用,从而调用者可见。
public class IntegerSet{
private /*@spec_public@*/ ArrayList<Integer> elements;
private /*@spec_public@*/ Integer max;
private /*@spec_public@*/ Integer min;
/*@
@ ...
@ assignable \nothing; // assignable(可赋值)
@ assignable \everything;
@ modifiable \nothing; // modifiable(可修改)
@ modifiable \everthing;
@ assignable elements;
@ modifiable elements;
@ assignable elements, max, min;
@ modifiable elements, max, min;
@*/
}
课程区分两类方法:全部过程和局部过程。前者对应着前置条件恒为真,即可以适应于任意调用场景;后者则提供了非恒真的前置条件,要求调用者必须确保调用时满足相应的前置条件。
从设计角度,软件需要适应用户的所有可能输入,因此也需要对不符合前置条件的输入情况进行处理,往往对应着异常处理。从规格的角度,JML区分这两种场景,分别对应正常行为规格(normal_behavior)和异常行为规格(expcetional_behavior)。
/*@ public normal_behavior
// public normal_behavior 表示接下来的部分对方法的正常功能给出规格
@ requires z <= 99;
@ assignable \nothing;
@ ensures \result > z;
@ also
// also表示还有异常功能存在或者子类重载父类需要补充规则
@ public exceptional_behavior
// exceptional_behavior 表示接下来的部分对方法的正常功能给出规格
@ requires z < 0;
@ assignable \nothing;
@ signals (IllegalArgumentException e) true;
@*/
public abstract int cantBeSatisfied(int z) throws IllegalArgumentException;
这个例子其实是错误的!
- signal
signals (***Exception e) b_expr
// 当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e
signals_only ***Exception e
// 满足前置条件则抛出异常
public abstract class Student {
/** A specification that can't be satisfied. */
//@ public model non_null int[] credits;
/*@ normal_behavior
@ requires z >=0 && z <= 100;
@ assignable \nothing;
@ ensures \result == credits.length;
@ also
@ exceptional_behavior
@ requires z < 0;
@ assignable \nothing;
@ signals_only IllegalArgumentException;
@ also
@ exceptional_behavior
@ requires z > 100;
@ assignable \nothing;
@ signals_only OverFlowException;
@*/
public abstract int recordCredit(int z) throws IllegalArgumentException,
OverFlowException;
}
// 根据输入参数的取值范围抛出不同的异常
类型规格
约束限制
类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。
- 不变式限制(invariant)
不变式(invariant)是要求在所有可见状态下都必须满足的特性。
invariant P
可见状态(visible state):
1. 对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
2. 在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
3. 在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
4. 在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
5. 在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
6. 在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻
凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态(完整可见)。在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调在任意可见状态下都要满足不变式。
public class Path{
private /*@spec_public@*/ ArrayList <Integer> seq_nodes;
private /*@spec_public@*/ Integer start_node;
private /*@spec_public@*/ Integer end_node;
/*@ invariant seq_nodes != null &&
@ seq_nodes[0] == start_node &&
@ seq_nodes[seq_nodes.legnth-1] == end_node &&
@ seq_nodes.length >=2;
@*/
}
静态不变式(static invariant) 和 实例不变式(instanceinvariant)。其中静态不变式只针对类中的静态成员变量取值进行约束,而实例不变式则可以针对静态成员变量和非静态成员变量的取值进行约束。可以在不变式定义中明确使用instance invariant
或 static invariant
来表示不变式的类别。
- 状态变化约束(constraint)
用constraint来对前序可见状态和当前可见状态的关系进行约束。
public class ServiceCounter{
private /*@spec_public@*/ long counter;
//@ invariant counter >= 0;
//@ constraint counter == \old(counter)+1;
}
static constraint指涉及类的静态成员变量,而instance constraint则可以涉及类的静态成员变量和非静态成员变量。
方法与类型规格的关系
静态成员初始化 | 有状态静态方法 | 有状态构造方法 | 有状态非静态方法 | |
---|---|---|---|---|
static invariant | 建立 | 保持 | 保持 | 保持 |
instance invariant | (无关) | (无关) | 建立 | 保持,除非是finalizer方法 |
注:“建立”的含义是静态成员建立了满足相应不变式的类或对象状态。“保持”的含义是如果方法执行前不变式满足,执行后还应该满足相应的不变式。
静态成员初始化 | 有状态静态方法 | 有状态构造方法 | 有状态非静态方法 | |
---|---|---|---|---|
static constraint | (无关) | 遵从 | 遵从 | 遵从 |
instance constraint | (无关) | (无关) | (无关) | 遵从 |
注:“遵从”的含义是成员变量的当前取值和上一个取值之间的关系满足constraint的规定,即“遵从规定”。
一个完整例子
public class Student {
// 几个变量是私有的
private /*@ spec_public @*/ String name;
//@ public invariant credits >= 0;
private /*@ spec_public @*/ int credits;
/*@ public invariant credits < 180 ==> !master &&
@ credits >= 180 ==> master;
@*/
private /*@ spec_public @*/ boolean master;
/*@ requires sname != null;
@ assignable \everything;
@ ensures name == sname && credits == 0 && master == false;
@*/
public Student (String sname) {
name = sname;
credits = 0;
master = false;
}
/*@ requires c >= 0;
@ ensures credits == \old(credits) + c;
@ assignable credits, master;
@ ensures (credits > 180) ==> master
@*/
public void addCredits(int c) {
updateCredits(c);
if (credits >= 180) {
changeToMaster();
}
}
/*@ requires c >= 0;
@ ensures credits == \old(credits) + c;
@ assignable credits;
@*/
private void updateCredits(int c) {
credits += c;
}
/*@ requires credits >= 180;
@ ensures master;
@ assignable master;
@*/
private void changeToMaster() {
master = true;
}
/*@ ensures this.name == name;
@ assignable this.name;
@*/
public void setName(String name) {
this.name = name;
}
/*@ ensures \result == name;
@*/
public /*@ pure @*/ String getName() {
return name;
}
}