\result
:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
\old(expr)
:表示一个表达式expr
在相应方法执行前的取值,该表达式涉及到评估expr
中的对象是否发生变化。
如果是引用(如hashmap),对象没改变,但进行了插入或删除操作。v和odd(v)也有相同的取值。
\not_assigned(x,y,...)
:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。
\not_modified(x,y,...)
:该表达式限制括号中的变量在方法执行期间的取值未发生变化。
\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。
\forall
:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。//针对任意 0<=i
//这个表达式如果为真( true ),则表明数组a实际是升序排列的数组。
(\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j])
\exists
:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。//针对0<=i<10,至少存在一个a[i]<0。
(\exists int i; 0 <= i && i < 10; a[i] < 0)
\sum
:返回给定范围内的表达式的和。//中间的i是对范围的限制,计算用最右边的参数
(\sum int i; 0 <= i && i < 5; i)
//即0+1+2+3+4==10
(\sum int i; 0 <= i && i < 5; i*i)
//即0+1+4+9+16==30
\product
:返回给定范围内的表达式的连乘结果。 (\product int i; 0 < i && i < 5; i)
\max
:返回给定范围内的表达式的最大值。 (\max int i; 0 <= i && i < 5; i)
\min
:返回给定范围内的表达式的最小值。 (\min int i; 0 <= i && i < 5; i)
\num_of
:返回指定变量中满足相应条件的取值个数。可以写成(\num_of T x; R(x);P(x))
,其中T为变量x的类型,R(x)为x的取值范围;P(x)定义了x需要满足的约束条件。从逻辑上来看,该表达式也等价于(\sum T x;R(x)&&P(x);1)
。//给出(0,20]以内能够被2整除的整数个数,得到的数目为10
(\num_of int x; 0<x && x<=20;x%2==0)
可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。集合构造表达式的一般形式为: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。
JML可以正常使用java所定义的操作符,此外,还专门定义了以下四类。
E1<:E2
子类型操作符:如果类型E1是类型E2的子类型(sub type)或相同类型,则该表达式的结果为真,否则为假。任意一个类X,都必然满足X.TYPE<:Object.TYPE
。//真
Integer.TYPE<:Integer.TYPE
//假
Integer.TYPE<:ArrayList.TYPE
b_expr1<==>b_expr2
或b_expr1<=!=>b_expr2
等价关系操作符:其中b_expr1和b_expr2都是布尔表达式。
b_expr1==>b_expr2
或b_expr1<==b_expr2
推理操作符:相当于离散的->,只有(1,0)是false。
\nothing
或\everthing
变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。变量引用操作符经常在assignable
句子中使用,如 assignable \nothing
表示当前作用域下每个变量都不可以在方法执行过程中被赋值。
定义前置条件和满足后置条件的东西。
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
requires P;
其中requires
是JML关键词,表达的意思是“要求调用者确保P为真”。多个分开的requires是并列关系都要满足,或关系用requires P1||P2
;
public int removePath(Path p)
- p==null
- p!=null but p is not valid
- p is valid,but p
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures
是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。
ensures P;
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
assignable
(表示可赋值)或者modifiable
(可修改)。虽然二者有细微的差异,在大部分情况下,二者可交换使用。public class IntegerSet{
private /*@spec_public@*/ ArrayList<Integer> elements;
private /*@spec_public@*/ Integer max;
private /*@spec_public@*/ Integer min;
/*@
@ ...
@ assignable \nothing; 都不可赋值
@ assignable \everything; 都可赋值
@ modifiable \nothing; 都不可修改
@ modifiable \everthing; 都可修改
@ assignable elements; elements可赋值
@ modifiable elements; elements可修改
@ assignable elements, max, min; 这仨都可赋值
@ modifiable elements, max, min; 这仨都可修改
@*/
}
注1:JML不允许在副作用约束子句中指定规格声明的变量数据,因为这样的声明只是为了描述规格,并不意味实现者一定要实现这样的数据。
注2:JML提供了/*@spec_public@*/
来注释一个类的私有成员变量,表示在规格中可以直接使用,从而调用者可见。
(/*@ pure @ */)
pure
方法的返回结果public /*@ pure @*/ String getName();
//@ ensures \result == bachelor || \result == master;后置:确保返回值
public /*@ pure @*/ int getStatus();
//@ ensures \result >= 0; 后置:确保返回值
public /*@ pure @*/ int getCredits();
/*@ requires c >= 0; 前置:约束c
@ ensures getCredits() == \old(getCredits()) + c;后置:getCredits()变化
@*/
public void addCredits(int c);
forall
和exists
/*@ requires size < limit && !contains(elem);前置:数目不能超过限制,不含重复元素
@ ensures \result == true; 后置:确保结果为真
@ ensures contains(elem); 后置:确保把参数elem对应的整数加入容器
@ ensures (\forall int e; 后置:确保对于容器的所有的
@ e != elem; 不是elem的元素
@ contains(e) <==> \old(contains(e))); 仍在容器中
@ ensures size == \old(size) + 1; 后置:确保数目+1
@*/
public boolean add(int elem) {/*...*/}
/*@ ensures !contains(elem); 后置:确保elem不在容器中了(被移除或本没有)
@ ensures (\forall int e; 后置:确保对于容器中所有的
@ e != elem; 不是elem的元素
@ contains(e) <==> \old(contains(e)));仍在容器中
@ ensures \old(contains(elem)) ==> size == \old(size) - 1; 后置:原来有elem,size-1
@ ensures !\old(contains(elem)) ==> size == \old(size); 后置:原来没elem,size不变
@*/
public void remove(int elem) {/*...*/}
public normal_behavior
和public exception_behavior
public
,指相应的规格在所在包范围内的所有其他规格处都可见。also
,这里指除了正常功能规格外,还有一个异常功能规格。有两种使用
also
的场景:
- 父类中对相应方法定义了规格,子类重写了该方法,需要补充规格,这时应该在补充的规格之前使用also;
- 一个方法规格中涉及多个功能规格描述,正常功能规格或者异常功能规格,需要使用also来分隔。
同一个方法的正常功能前置条件和异常功能前置条件一定不重叠。
/*@ public normal_behavior 对正常功能给出规格
@ requires z >= 0; 前置:z>0
@ assignable \nothing;
@ ensures \result > z;
@ also
@ public exceptional_behavior 对异常功能给出规格
@ requires z < 0; 前置:z<0
@ assignable \nothing;
@ signals (IllegalArgumentException e) true; 只要z<0会抛出异常
@*/
public abstract int cantBeSatisfied(int z) throws IllegalArgumentException;
signals (***Exception e) b_expr
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程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。
从面向对象角度来看,类或接口包含数据成员和方法成员的声明及或实现。不失一般性,一个类型的成员要么是静态成员(static member),要么是实例成员(instance member)。
一个类的静态方法不可以访问这个类的非静态成员变量(即实例变量)。静态成员可以直接通过类型来引用,而实例成员只能通过类型的实例化对象来引用。因此,在设计和表示类型规格时需要加以区分。
invariant P
invariant
为关键词, P
为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。对象o的状态都是可见状态:
- 对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
- 在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
- 在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
- 在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
- 在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
- 在未处于对象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;
@*/
}
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;
}
}