一、关于JML语言
1、JML简介:Java建模语言(Java Modeling Language(JML)),是一种进行详细设计的符号语言,用于对Java程序进行规格化设计,属于行为接口规格语言。JML的两种主要用法:开展规格化设计;针对已有的代码实现,书写其对应的规格,提高代码的可维护性。
2、基础知识梳理:
(1)注释结构:以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式:行注释,表示方法为// @annotation;块注释,表示方法为/* @ annotation @ */。
(2)JML表达式:原子表达式:
esult表达式:表示一个非void方法执行所获得的返回值。
old(expression)表达式:用来表示一个表达式expression在相应方法执行前的取值。
量化表达式:forall表达式:全称量词修饰的表达式。(forall int i,j;0<=i&&i<j&&j<10;a[i]<a[j]),意思是针对任意0<=i<j<10,a[i]<a[j]。
exists表达式:存在量词修饰的表达式。(exists int i;0<=i&&i<10;a[i]<0),表示针对0<=i<10,至少存在一个a[i]<0。
sum表达式:返回给定返回内的表达式的和。(sum int i;0<=i&&i<5;i),表示计算[0,5)范围内的整数的和。
max表达式:返回给定范围内的表达式的最大值。
min表达式:返回给定范围内的表达式的最小值。
操作符:子关系类型操作符:E1<:E2,如果类型E1是类型E2的子类型,则该表达式为真。
等价关系操作符:expr1<==>expr2或expr1<=!=>expr2,表示两个表达式等价或不等价。
推理操作符:expr1==>expr2,expr1可以推出expr2。
变量引用操作符:
othing指示一个空集,everything指示一个全集。
(3)方法规格:前置条件:通过requires子句来表示。
后置条件:通过ensures子句来表示,也可通过signals子句来抛出异常。
副作用范围限定:使用关键词assignable或modifiable。
(4)类型规格:不变式invariant,是要求在所有可见状态下都必须满足的特性(凡是会修改成员变量的方法在执行期间,对象的状态都不是可见状态)。
状态变化约束constraint,对前序可见状态和当前可见状态的关系进行约束。
3、JML应用工具链:
由于本人刚刚接触JML,对于JML测试工具的使用更是接触不多。在本单元中我们主要用到的JML应用工具有:1、OpenJML:可以进行JML语法检查、静态检查(不依赖于JML)、运行时检查。2、JMLUnitNG:根据JML进行自动测试样例生成。
其他JML工具:JMLdoc、JMLunit,都是基于JML规范的开源JML工具。
二、OpenJML+JMLUnitNG+IDEA进行基于JML的自动测试
主要步骤如下:1、下载OpenJML,解压。
2、下载JMLUnitNG,得到jmlunitng.jar。
3、为了使用方便,将第一步解压得到的文件夹中的Solvers-windows文件夹、三个jar包,以及jmlunitng.jar放在同一个文件夹下。
4、生成测试文件。首先将要进行自动测试的java文件编写规格,放在某个特定文件夹下。假设此java文件(设为test.java)与之前的jmlunitng.jar处在同一目录下,那么进入该目录下,通过调用命令$ java -jar jmlunitng.jar test.java,自动地在当前目录下生成相关测试文件(test_JML_Test.java等)。
5、编译。这一步很关键,因为在第四部中生成的测试文件不能直接在IDEA中运行,需要通过使用命令行进行编译,使主文件带有运行时检查。首先用javac编译JMLUnitNG的生成文件:$ javac -cp jmlunitng.jar **.jar;之后用jmlc编译自己的源文件,生成带有运行时检查的class文件:$ java -jar openjml.jar -rac test.java。
6、运行。可以使用命令行进行测试文件的运行,但是有些系统配置的编译器版本过低,并且测试文件有可能会使用第三方jar包,导致命令行运行文件使用不便。本人采用的方法简单粗暴,将第五步中得到的所有文件(包括.java文件、.class文件),一并放入IDEA工程中进行运行。
7、操作及运行结果展示。为了方便,本人只写了一个非常简单的类进行JML测试。操作及运行结果如下:
测试程序
命令行操作过程
IDEA运行结果展示
测试结果的第一行是 racEnabled 的测试,意在检测我们的主文件是否带有 JML 的运行时检查,若没有则跳过所有测试。总共进行了10次测试,进行了整数边界测试、空集测试等特殊情况测试,覆盖全面。自动测试验证通过。
三、架构设计
1、第一次作业是实现一个简单的路径管理容器类(PathContainer),通过构造HashMap<Integer,Path>和HashMap<Path,Integer>双Map结构进行路径管理,实现Path和Pathid之间的映射。通过HashMap<Integer,Integer>实现不同节点的管理,由节点id映射到该节点出现的次数。
2、第二次作业是在第一次作业的基础上实现一个由不同路径构成的图类(Graph)。本次作业中不同路径中的id相同的点可以进行合并,使得图中的节点数量很少。在第一次作业架构的基础上,我对PathContainer类 进行功能扩展,通过构造HashMap<Integer,ArrayList<Integer>>实现图的邻接表形式,节约内存空间。通过dijsktra算法得到任意两节点的最短路径长度,以HashMap<Integer,HashMap<Integer,Integer>>的数据结构存储,由起始节点id映射到一个关于该节点的距离HashMap,该HashMap中的映射关系为目标节点映射距离。通过两次HashMap访问,可以得到任意两点的最短距离(若两点不连通,则Map中的距离区域存储100000)。
3、第三次作业,按照惯例又是一次难度上的飞跃。本次作业要求大家在第二次作业的基础上构建一个地铁管理系统(RailwaySystem),实现任意两节点间的最短距离查询、最低票价查询、最少换乘次数查询、最低不满意度查询,以及整个地铁图的连通块个数查询。在本次作业中,我采用的构图方法是不合并节点,不同路径中的id相同的节点,实际上是不同的节点。我构造了一个Node类,包含着节点的id、所属路径的id以及邻接节点表、邻接边权表。通过构造ArrayList<Node>来实现图的邻接表形式存储。针对不同的查询功能,构造对应的图,每种图的边权的计算方式也不同。本次作业采用了继承方式,RailwaySystem类继承了上次作业中的Graph类,实现架构上的迭代。本次作业引入了缓存思想(cache),每次计算最短路径长度,都会算出一组最短路径长度,存入一个缓存区中(HashMap<Integer,HashMap<Integer,Integer>>)。这一组最短路径长度的数据有可能会在之后的查询操作中被使用。至于连通块个数查询,我采用了dfs算法,实现起来比较容易,我也没有想到性能更好的算法。
四、bug及修复情况
这一单元的bug,怎么说呢,比较尴尬。如果是正确性bug,那就寻根究底找到错误原因,进行改正就好。这种bug比较容易修复,可我三次作业中都没有出现这种bug。棘手的是time limit bug,超时错误。如果发生了这种bug,仅仅修改一处或几处代码,对于性能的提升简直是杯水车薪。必须要进行重构,甚至要推翻重做。最可怕的是,当你推翻重来,实现了一个你认为已经很好地优化了的架构,却看到重新提交的结果仍然显示存在超时bug,甚至本来通过的点却过不了了。那么欢迎你加入我们乐观家族,反向debug。乐观一点,离开了oo,生活也可以很美好。
在第一次、第三次作业中我很不幸地遇到了超时错误。第一次作业中修复这种bug还比较简单,我只是简单重构了一下存储结点的数据结构,并将时间复杂度做了一个简单的分摊,就获得了数倍的性能提升,轻松地修复了bug(当初架构上确实问题很大,考虑得不够周全)。第三次作业的超时bug我最后并没有成功修复。
三次作业中,我的前两次作业在互测环节既没有被找出bug,也没有发现别人的bug。第三次作业的互测环节中我不出所料地被找出了超时bug,同时我找出了同组玩家的两个正确性bug。
五、心得体会
本次JML(图论)作业单元让人获益匪浅。我不仅在第一次作业中学到了很多JML的知识,更是在第二、三次作业中复习了数据结构与算法这门去年曾经折磨过我的课,并通过自学学到了一些新的知识(并查集、网络流,虽然我并没有用到)。说心里话,本单元虽然是JML专题单元,却有点名不副实,起名为图论与算法单元更加切合实际。JML的相关知识,在我看来,仅仅在第一次作业和博客作业中得到了应用,缺乏真正意义上的与设计架构的结合过程。第二次作业,尤其是第三次作业,很少有人去关注规格、关注JML,大家都在绞尽脑汁地学习与权衡不同的算法、设计性能更好的架构来让自己的强测分更好看,让自己不在互测中吃瘪。
不过在本单元中大家还是学到了许多东西。在令人精疲力尽、焦头烂额的三次oo作业之后,回过头来发现自己进步了不少、收获了不少,是一件多么令人开心的事啊!