第三单元总结
一、JML语法基础
1、JML表达式
(一)原子表达式
关键字 | 含义 |
---|---|
\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 |
全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。(\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j]) |
\exists |
存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。(\exists int i; 0 <= i && i < 10; a[i] < 0) |
\sum |
返回给定范围内的表达式的和。(\sum int i; 0 <= i && i < 5; i*i) |
\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) 。(\num_of int x; 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) && 100 < i.intValue() }
表示构造一个JMLObjectSet对象,其中包含的元素类型为Integer,该集合中的所有元素都在容器集合s中出现(注:该容器集合指Java程序中构建的容器,比如ArrayList),且整数值大于100。
(四)操作符
JML可以正常使用javaa所定义的操作符,以及JML定义的四种操作符。
-
E1<:E2
若E1是E2的子类或同类型,则结果为真,反之为假。
//True Integer.TYPE<:Integer.TYPE //False Integer.TYPE<:HashSet.TYPE
-
b_expr1<==>b_expr2
与b_expr1<=!=>b_expr2
等价操作符,其中b_expr1与b_expr2都是布尔表达式。
-
b_expr1==>b_expr2
与b_expr1<==b_expr2
推理操作符,与离散数学相同。
-
\nothing
与\exerythin
变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。变量引用操作符经常在
assignable
句子中使用,如assignable \nothing
表示当前作用域下每个变量都不可以在方法执行过程中被赋值。
2、方法规格
(一)前置条件
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
requires P;
其中requires
是JML关键词,表达的意思是“要求调用者确保P为真”。多个分开的requires是并列关系都要满足,或关系用requires P1||P2
;
(二)后置条件
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures
是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。
ensures P;
(三)副作用
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
- 从方法规格的角度,必须要明确给出副作用范围。
- JML提供了副作用约束子句,使用关键词
assignable
(表示可赋值)或者modifiable
(可修改)。虽然二者有细微的差异,在大部分情况下,二者可交换使用。 - 副作用约束子句共有两种形态
- 用JML关键词来概括,不指明具体的变量;
- 指明具体的变量列表
public class IntegerSet{
private /*@spec_public@*/ ArrayList 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.Junit
JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。
本单元三次实验中,利用Junit构建测试样例,不仅能测试代码正确性,还能增加对于题意的理解,更方便了互测过程中找到其他同学的bug。
三、SMT Solver
SMT( Satisfiability modulo theories )求解器在形式化方法、程序语言、软件工程、以及计算机安全、计算机系统等领域得到了广泛应用。
1.语法检查
通过指定相关文件路径,输入如下命令
java -jar PATH1\opemjml.jar -exec PATH2\z3-4.7.1.exe -check PATH3\code.java
`即可开始验证,若无jml语法错误,无输出。以Person.java 为例,验证无误。
当对文件做一些错误修改,比如删去一个分号。再运行一次语法检查。
得到提示,缺少一个分号。
2.静态验证
java -jar Path1\openjml.jar -exec Path2\Solvers-windows\z3-4.7.1.exe -esc Patht3\code.java -encoding UTF-8
以自主编写的Calculate类为例
public class Calculate {
/*@ public normal_behavior
@ requires b != 0;
@ ensures \result == a / b;
@ also
@ public exceptional_behavior
@ signals (ArithmeticException e) b == 0;
@*/
public static double div(int a, int b) {
if (b == 0) {
throw new ArithmeticException();
}
else {
return (a / b);
}
}
/*@ public normal_behavior
@ ensures \result == ((a >= b) ? a : b);
@*/
public static double max(int a,int b) {
return Math.max(a,b);
}
/*@ public normal_behavior
@ ensures \result == ((a <= b) ? a : b);
@*/
public static double min(int a,int b) {
return Math.min(a,b);
}
}
实现了取最大值,最小值,以及除法相关功能。
静态验证结果如下。
3.动态验证
java -jar Path1\openjml.jar -rac code.java
java -cp Path2\jmlruntime.jar; code
仍以Calculate为例
public static void main(String[] args) {
System.out.println(div(1,2));
System.out.println("max between 1 and 2" + max(1,2));
System.out.println("min between 2 and 1" + min(1,2));
System.out.println(div(1,0));
}
若div方法中为考虑除0,动态验证结果如下。
测试表明div方法实现产生效果有误。
三、JMLUnitNG\JMLUnit
以Person结构为例,自动生成测试用例。
生成代码
java -jar Path/jmlunitng.jar src/MyPerson.java src/com/oocourse/spec3/*
结果显示hashmap等应当增加初始化类型。
修改代码后生成了大量测试文件。
运行MyPerson_JML_Test文件后,部分结果为
。。。
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
Passed: <>.stronglinkto(null)
===============================================
Command line suite
Total tests run: 487, Failures: 37, Skips: 0
===============================================
自动生成样例多为边界条件,如null与int型边界值。有37个点未通过,是因为某些方法未考虑输入为null的情况。
四、架构设计分析
第一次作业
第一次作业设计较为简单,耐心理解题意即可过关。
- 题目较为简单,未对架构过多考虑
- 多使用java自带的hashmap,hashset等高效查找容器,查找、修改速度快,但遍历速度慢,在第三次作业中暴露出问题。
第二次作业
第二次作业需要利用缓存机制,更新sum等等信息,避免反复查询反复遍历的问题
- 缓存机制减少遍历时间
第三次作业
第三次作业需要考虑高效算法的实现,强测的惨痛结果表明照抄jml的写法注定是死路一条。
- 并查集算blocksum
- 迪杰斯特拉+堆优化
- Arraylist + Hashmap 等多种容器结合使用
五、相关bug及修复
第一次作业
第一次作业无bug,互测也为找到同学bug。
第二次作业
第二次作业犯了一个十分低级的错误,在计算平均值时直接除0,导致强测爆炸。修改了几行代码后就顺利通过。气得我吐血
第三次作业
第三次作业在tarjan上花费太多时间,导致并查集与堆优化这两方面未估计到,导致强测直接爆了10个ctle。。。因为强测成绩太低,互测刀了个爽。在第三次作业,我才意识到偷懒照抄jml实现,需要付出惨痛的代价。
六、心得体会
-
在前两次作业中,算是深刻体会到了规格“高冷”的特点。用最为严谨、规范的语言描述代码的作用,其余的实现全靠程序员自己。有种工科生的浪漫。
括号嵌套一多眼睛都要瞎掉 -
在第三次作业里,则通过血淋淋的事实深刻体会到jml的实现不仅仅是结果正确,还有效率问题。一个合格的程序员不仅要保证程序的正确性,还要保证可行性。
-
JML规格的标准化还能够有利于团队开发,通过严谨规范的要求能够保证每个员工完成自己部分后,整体项目不会出现协调不够而引发的低级错误。