一、JML简单引导
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。 规范的JML语言描述了正确的Java程序的功能性要求,但如何实现,以及实现的性能如何就交给了程序猿(卑微的我们)自己了。
理论基础
JML以javadoc注释的方式来表示规格,有行注释和块注释两类。行注释以“//@”开头;块注释以“/@”开头,而每一行又以“@”开头。
一个完整的方法规格包括正常行为(normal_behavior)和异常行为(exceptional_behavior)。个行为之间用"also"语句联系起来。
正常行为下定义的规格可包含前置条件、后置条件和副作用范围限定。
1、前置条件:通过requires子句表示,后跟逻辑表达式A,表示该方法的这一分支行为执行前要求满足“A为真”。
2、后置条件:通过ensures子句表示,后跟逻辑表达式B,表示该方法的这一分支行为执行后保证满足“B为真”。
3、副作用范围限定:使用assignable和modifiable,后跟变量列表或者谓词"\nothing", "\everything",表示在该方法的这一分支行为执行过程中所罗列的变量是允许修改的。
这就相当于开发者与调用者之间签订的契约。调用者使用该方法时必须满足开发者所要求的前置条件,而开发者所构造的函数必须满足调用者所提出的后置条件。
在写前置条件、后置条件时,可能还会用到如下关键词:
1、\result:表示对于函数返回值。
2、\old(a):表示方法行为执行前的变量或参数a的值。
3、\exists v; exp1; exp2:存在量词语句,表示存在满足条件exp1的变量v,满足条件exp2。
4、\forall v; exp1; exp2:全称量词语句,表示所有满足条件exp1的变量v,都满足条件exp2。
异常行为下定义的规格通常使用关键词"signals"描述的语句来实现:
signals (Excepion e) (exp1);
。
表示如果方法执行前满足逻辑表达式exp1,则抛出异常e。
应用工具链
1、OpenJML:Java程序的程序验证工具,可用来检查所言JML注释的规范。
2、JMLUnitNG / JMLUnit:用于带有JML注释的Java代码的自动化单元测试生成工具。
想了解更多工具链,请点击这里。下文将以实例对这两个工具进行介绍
二、部署SMT-Solver
下载解压好OpenJML。
OS : windows10
Java version : 1.8.0_221(之前重装IDEA时又重下了java,根据同学交流,好像有的版本不太行。。。)
OpenJML version : z3-4.7.1.exe
先使用命令:
java -jar .\openjml.jar -exec .\Solvers-windows\z3-4.7.1.exe -esc F:\0\OO\week12\HW12\src\com\oocourse\spec3\main\*.java
想直接对课程组提供的接口JML代码进行检验,找到了100个错误,这是因为项目下的代码并不都在这个文件夹里,致使对于其他文件的依赖并不能建立,结果如下:
于是改用指令:
G:\openjml>java -jar .\openjml.jar -exec .\Solvers-windows\z3-4.7.1.exe -dir F:\0\OO\week12\HW12\src
对整个项目的代码文件进行检验,便可以消除那些无意义的报错,定位到两个错误:
大概没能把people.length识别为int类型。。。
(可见课程组的JML规格写得还是很规范的,夸!)
三、部署JMLUnit
下载好JMLUniting
我先将Group, Person, MyGroup, MyPerson以及一些异常类重新打包,并对其中的import作相应修改,执行指令:
java -jar jmlunitng.jar test/MyGroup.java
会出现各种奇奇怪怪的错误,部分截图如下
jmlunitNG将各种容器类都识别为非法类型。。。于是便手动修改为数组,然而还是有很多编译器版本的警告和“无法找到类型”的警告。
和同学交流后发现好多人都遇到这个问题,无奈便重写了一个简单的JMLTest类。
package JMLTest;
public class JMLTest {
/*@ public normal_behaviour
@ ensures \result == (a + b) / 2;
*/
public static int average(int a, int b) {
return (a + b) / 2;
}
/*@ public normal_behaviour
@ ensures \result == a * b;
*/
public static int multiple(int a, int b) {
return a * b;
}
public static void main(String[] args) {
int a = 2000;
int b = 1019;
multiple(average(a, b), a);
}
}
执行:
java -jar jmlunitng-1_4.jar JMLTest\JMLTest.java
生成测试文件:
接着编译测试文件时出了问题。。。
与同学交流后可能是jdk版本问题。调了半天没能出来,只好放弃。
四、架构设计及bug分析
这个单元的作业模拟了一个社交网络中的小世界模型:个人——群组——社会。
第一次作业
第一次作业基本不用考虑性能,只要对JML规格认识清楚,没有看错、看漏要求(比如一些边界条件以及条件语句中的括号),应该就不会有什么问题。
这里最复杂的方法应该就是isCircle()了,需要判断两个人之间是否有通路,我在这里采用了BFS。
第二次作业
这次作业增加了群组Group类,除了增加一些add()和query()方法,Person类和Network类基本保持不变。
在对一个Group里的person相应属性求平均和方差时,可以经过数学推导得到简化公式,这样就避免了高复杂度的遍历。
由于存在指令非常多的测试点,在使用HashSet, HashMap, ArrayList这些容器时,可预先设置较大的容量,虽然对于某些小测试点可能牺牲了内存,但是可以有效减少扩容时带来的性能损失。
值得一提的是,这次作业对CPU性能的要求有了很大的提升,这就导致我在getRelatiionSum()和getVauleSum()中使用的无脑遍历这种O(n^2)复杂度的算法会出现tle。必须将相应的属性单独存储,在对person进行操作的同时,要对包含这个person的所有group进行更新。
我在自己写程序的时候注意到如果只将人的属性静态地在group中存储器来的话,那么先将人addtogroup后addrelation时就很有可能出现问题,于是也在讨论区中提醒了大家注意这一点。不过在互测时还是发现有同学没有注意,于是很不好意思地hack了几个点。
第三次作业
这次作业给Group类增加了“踢出群聊”的操作,只要按照第二次作业中“加入群聊”的步骤来写,基本就不会有问题。
在对character的计算有了新的发现——异或与加减的关系。就当我兴冲冲地想发在讨论区时,发现已经被同学抢先一步了,只能默默地点个赞。。。
此外,还在Network类中增加了不少查询图的属性的方法,这些是这次作业的关键所在。
这些方法对于算法性能的要求特别高,基本限定了每个函数要用哪个算法,否则就会tle。
中测还是一如既往的弱,结果我在强测中有不少点出现了CPUtle,我对于自己bug修复阶段的总结如下:
1、单纯的Dijkstra方法会CPUtle,于是增加了自定义类Relation,修改isCircle方法为优先队列的Dijkstra算法;
2、原本queryStrongLinked(以下简称qsl)方法是用DFS找环路实现的,这里修改为暴力枚举法;
3、修改了queryBlockSum方法中错误的判断;
4、基于上述修改对MyNetwork类中需要调整的小的部分也一起修改。
而在qsl方法中,我也尝试了Tarjan算法,但由于自己最开始不够理解,导致写出来有着单纯DFS的影子,所以最后还是放弃。对于暴力枚举的方法,在实现过程中也有很多细节要注意——比如了解LinkedList, ArrayList, HashSet这些容器的特点和区别,否则还是可能会出现CPUtle。
在bug修复后的类图如下:
五、心得体会
同时,对于开发人员而言,有一个统一的、规范的注释,既能避免歧义,也使得程序在后续的开发、维护、迭代、交互的过程中能够更容易的实现。
这个单元除了让我们较为深入的了解了JML语言之外,还巩固了离散数学中图论的知识,同时也对Java自带的各个容器有了更深的认识。
不得不说,这个单元可谓是收获满满。
(ps:除了非常难用的一些jml工具给人带来的不愉快)