JML单元
1.梳理JML语言的理论基础
概述
Java建模语言(Java Modeling Language,JML)是一种进行详细设计的符号语言,使人们用一种全新的方式来看待Java的类和方法。JML是一种用于Java模块的行为接口规格语言 。JML提供了用于正式描述Java模块行为的语义,从而避免了与模块设计人员意图相关的模糊性。JML继承了Eiffel、Larch和求精演算法的思想,其目标是提供严格的形式语义,同时仍然对任何Java程序员可用。我们可以使用各种工具来使用JML的行为规范,因为规范可以作为注释在Java程序文件中编写,或者存储在单独的规范文件中,所以使用JML规范的Java模块可以不受任何Java编译器的影响进行编译。同时在开发过程中,使用JML语言进行建模可以帮助我们跳出控制语句的约束,只从功能的角度给出类或方法的期望作用,内部数据的期望变化而不去考虑用什么方式来实现这种变化。通过这种抽象,我们在设计时就会减轻很多压力而专注于设计一个有好的功用的结构框架以供开发,提高效率。
JML表达式
原子表达式
表达式 | 描述 |
---|---|
\result | 表示一个非void 类型的方法执行所获得的结果 |
\old(expr) | 用来表示一个表达式expr 在相应方法执行前的取值 |
\not_assigned(x,y,...) | 用来表示括号中的变量是否在方法执行过程中被赋值 |
\not_modified(x,y,...) | 与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化 |
\nonnullelements(container) | 表示 container 对象中存储的对象不会有 null ,等价于下面的断言,其中\forall是JML的关键词,表示针对所有 i |
\type(type) | 返回类型type对应的类型(Class),如type( boolean )为Boolean.TYPE。TYPE是JML采用的缩略表示,等同于Java中的 java.lang |
\typeof(expr) | 该表达式返回expr对应的准确类型。如\typeof( false )为Boolean.TYPE |
量化表达式
描述 | |
---|---|
\forall | 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束 |
\exists | 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束 |
\sum | 返回给定范围内的表达式的和 |
\product | 返回给定范围内的表达式的连乘结果 |
\max | 返回给定范围内的表达式的最大值 |
\min | 返回给定范围内的表达式的最小值 |
\num_of |
集合表达式
操作符
操作符 | 描述 |
---|---|
E1<:E2 | 子类型关系操作符 |
b_expr1<==>b_expr2 或者b_expr1<=!=>b_expr2 |
等价关系操作符 |
b_expr1==>b_expr2 或者b_expr2<==b_expr1 |
推理操作符 |
\nothing指示一个空集,\everything指示一个全集 | 变量引用操作符 |
JML工具链
openjml:以SML Solver为组建,进行jml语法检查、代码静态检查、生成运行时测试类
JMLUnitNG/JMLUnit: 针对类自动生成测试样例并进行测试。
JML建模原则
用行为描述、前置条件、后置条件、影响范围对方法的行为建模,用不变式和状态变化约束对类的数据域建模,方法建模在类建模的基础之上。二者结合就可以对类的状态和行为进行完整的建模。
子类继承父类的规格,满足规格规定的行为。JML同样允许子类修改父类的方法规格,但必须满足以下条件:满足前置条件的集合必须扩大或不变,满足后置条件的集合必须减小或不变。
2.部署SMT Solver
SMT Solver是openjml的基础组件,在进行openjml在进行验证时调用SMT Solver。
安装与配置
OpenJML
从官网下载OpenJML:http://www.openjml.org/
之后按照安装提示正常安装。
JMLUnitNG
从官网下载 JMLUnitNG:http://insttech.secretninjaformalmethods.org/software/jmlunitng/
点击图中的1.4即可下载
命令格式
java -jar-check
语法检查实例
检查Person.java
java -jar openjml.jar -check E:\openjml\src\com\oocourse\spec3\main\Person.java
发现检查通过
检查main和exceptions下所有.java文件
这里留下了个非常奇怪的问题,这个INT#1不知道是啥,我猜可能是Integer对象,是不是说int和Integer不能用等号来比较?
于是,我把 people.length == 0 改成了 people.length.equals(0) 。结果报了更多的错。
在网上查询资料也没有相关结果,有大佬知道的话也请指正,谢谢!
静态检查实例
检查Person.java
java -jar openjml.jar -esc Solvers-windows\z3-4.7.1.exe -esc E:\openjml\src\com\oocourse\spec3\main\Person.java
检查通过。
动态检查实例
java -jar openjml.jar -exec Solvers-windows\z3-4.7.1.exe -rac E:\openjml\src\com\oocourse\spec3\main\Person.java
检查通过。
3.部署JMLUnitNG/JMLUnit
第一步:生成Group的测试文件
java -jar jmlunitng.jar test/MyGroup.java
可以发现在下面的代码中报错非法的类型开始:
private HashMappersonIdToPersonMap = new HashMap<>();
运行结果如下所示:
原因在于HashMap实例化时使用了泛型类,将其补全:
private HashMappersonIdToPersonMap = new HashMap ();
补全泛型代码后,发现不再报错:
第二步:编译测试文件
javac -cp jmlunitng.jar test/*.java
运行结果如下所示:
第三步:运行测试文件
java -jar jmlunitng.jar test.MyGroup_JML_Test
运行结果在命令行中显示如下:
以文本形式表示:
[TestNG] Running: Command line suite Failed: racEnabled() Passed: constructor MyGroup(-2147483648) Passed: constructor MyGroup(0) Passed: constructor MyGroup(2147483647) Passed: <>.addPerson(null) Passed: < >.addPerson(null) Passed: < >.addPerson(null) Passed: < >.addRelation(-2147483648) Passed: < >.addRelation(-2147483648) Passed: < >.addRelation(-2147483648) Passed: < >.addRelation(0) Passed: < >.addRelation(0) Passed: < >.addRelation(0) Passed: < >.addRelation(2147483647) Passed: < >.addRelation(2147483647) Passed: < >.addRelation(2147483647) Passed: < >.delPerson(null) Passed: < >.delPerson(null) Passed: < >.delPerson(null) Passed: < >.equals(null) Passed: < >.equals(null) Passed: < >.equals(null) Passed: < >.equals(java.lang.Object@7d4793a8) Passed: < >.equals(java.lang.Object@5479e3f) Passed: < >.equals(java.lang.Object@66133adc) Passed: < >.getAgeMean() Passed: < >.getAgeMean() Passed: < >.getAgeMean() Passed: < >.getAgeVar() Passed: < >.getAgeVar() Passed: < >.getAgeVar() Passed: < >.getConflictSum() Passed: < >.getConflictSum() Passed: < >.getConflictSum() Passed: < >.getId() Passed: < >.getId() Passed: < >.getId() Passed: < >.getPeopleSum() Passed: < >.getPeopleSum() Passed: < >.getPeopleSum() Passed: < >.getRelationSum() Passed: < >.getRelationSum() Passed: < >.getRelationSum() Passed: < >.getValueSum() Passed: < >.getValueSum() Passed: < >.getValueSum() Failed: < >.hasPerson(null) Failed: < >.hasPerson(null) Failed: < >.hasPerson(null) =============================================== Command line suite Total tests run: 49, Failures: 4, Skips: 0 ===============================================
分析自动生成的测试数据
观察测试数据,发现大概可以分为三类
- 空指针null。这类测试数据fail的原因是输入没有满足规格中所规定的前置条件,因此不算错
- 极端数据,如INT_MAX,INT_MIN
- 调用空参数的方法
4.梳理自己的架构设计
第一次作业
架构图
分析
第一次作业很简单,只需要按照给出的JML规格完成对应的设计即可。
存储结构使用hashMap进行存储,检索时可以做到O(1)复杂度。重点的方法isCircle()使用BFS,因为考虑到DFS是递归结构,递归过深可能会爆掉函数调用栈。
第二次作业
架构图
分析
第二次作业相较于第一次作业的变化就是增加了缓存机制。
比如在MyGroup类里加入一个缓存属性ageSum,在addPerson()中更新 ageSum = ageSum + person.getAge() ,这样只会在addPerson()时有O(1)的复杂度,并且查询ageSum时也是O(1)的复杂度。
如果不使用缓存机制,会在查询时有 O(n) 的复杂度,有可能会TLE。
查询valueSum和relationSum也是同理。
第三次作业
架构图
分析
第三次相较于第二次难度骤然增加,添加了许多图论的知识。
比如queryBlockSum(),必须要采用并查集的算法,再想使用挨个BFS\DFS的算法肯定是不行的,并且能缓存的尽量缓存。
本次的queryMinPath()方法,只能采用dijkstra算法,而且不能用朴素的dijkstra(血的教训),必须得用堆优化的dijkstra。
还有queryStrongLinked()方法,我采用的是Tarjan算法,寻找双连通分量,然后判断两个点是否在一个双连通分量里边。
不知道为什么网上的教程都说存点是不对的,只能存边。可能是他们认为割点可能存在于多个双连通分量里,这样可能导致错误。但是只需要注意在寻找到一个双连通分量后不把割点弹出栈就可以了,反正我这么做这个方法在强测没问题。
5.分析代码实现的bug和修复情况
第一次作业
第一次作业出现了bug,原因在于自己没有仔细阅读规格,少看了isLinked的规格里还有id相等的判断条件,导致错误,加上后就修复了。
第二次作业
第二次作业无bug。
第三次作业
第三次作业使用了朴素的dijkstra算法,虽然已经进行了一些小优化,最终仍有3个点出现了TLE的情况,且超时都很轻微(说不定再测一次就过了)。还是只能怪自己偷懒,摸了。
改为堆优化的dijkstra算法后秒过。
6.心得体会
- JML只是确定了各个类和方法的规则,至于内部的运作方式、代码架构和具体实现还是要自己理解,不能机械的照搬JML,在保证正确性的基础上算法应尽可能保证复杂度(时空效率)。
- JML一定要仔细看!一定要仔细看!一定要仔细看!
- JML相关的工具不是很友好,比如OpenJML强行要求JDK8,使用高版本的JDK14反而不行。
- 这个单元的助教是个狼人,一定是ACMer(本人弱鸡怕了)。