第三单元作业总结
一、JML语言的理论基础
1. 综述
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言,属于行为接口规格语言。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,还可以通过SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
一般而言,JML有两种主要的用法:一是开展规格化设计,从而使实现更加逻辑严格;二是针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。
在我们本单元的作业中,我们主要是根据给出的规格进行规格化设计,并逐步体验了实际OOP开发中,一个功能模块从低层次抽象到面向实际需求的演化过程。
虽然很数据结构,还日常TLE。
2. 注释结构
JML有行注释和块注释两种注释方式,一般放在被注释成分的近邻上部,具体写法如下所示。
1 //@ ensures \result == elements.length; 2 3 /*@ public normal_behavior 4 @ requires elements.length >= 1 5 @ assignable \nothing; 6 @ ensures \result == elements.length 7 @*/
3. 常用表达式
-
-
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
-
\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。为减少出错,使用\old时应扩起整个表达式。
-
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。 eg: (\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j]);
-
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。 eg: (\exists int i; 0 <= i && i < 10; a[i] < 0);
-
\sum表达式:返回给定范围内的表达式的和。
-
\product表达式:返回给定范围内的表达式的连乘结果。
-
\max表达式:返回给定范围内的表达式的最大值。
-
\min表达式:返回给定范围内的表达式的最小值。
-
\num_of表达式:返回指定变量中满足相应条件的取值个数。
-
推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1 。对于表达式 b_expr1==>b_expr2 而言,当 b_expr1==false ,或者 b_expr1==true 且 b_expr2==true 时,整个表达式的值为 true 。
-
4. 方法规格
-
-
-
前置条件(pre-condition) :要求调用者确保P为真 requires P;
-
副作用范围限定(side-effects):方法在执行过程中会修改对象的属性数据或者类的静态成员数据 assignable elements;
-
后置条件(post-condition) :方法实现者确保方法执行返回结果一定满足谓词P的要求 ensures P;
-
-
异常行为规格 (expcetional_behavior)
-
抛出异常(signals子句): signals (***Exception e) b_expr;
-
抛出异常(signals_only子句): signals_only (***Exception e);
-
-
pure关键词:纯粹访问性的方法。不会对对象的状态进行任何改变,也不需要提供输入参数,无需描述前置条件,也不会有任何副作用,且执行一定会正常结束,可以使用简单的(轻量级)方式来描述其规格。
-
also关键词:用于补充子类重写父类中已有规格的方法和分隔一个方法规格中涉及的多个功能规格描述。
-
方法规格举例:
-
1 /*@ public normal_behavior 2 @ requires z <= 99; 3 @ assignable \nothing; 4 @ ensures \result > z; 5 @ also 6 @ public exceptional_behavior 7 @ requires z < 0; 8 @ assignable \nothing; 9 @ signals (IllegalArgumentException e) true; 10 @*/
5.类型规格
-
-
不变式invariant :要求在所有可见状态下都必须满足的特性 invariant P
;
-
状态变化约束constraint: 对象的状态在变化时满足的一些约束,本质上也是一种不变式 constraint P 。
-
类型规格举例:
-
1 //@ invariant counter >= 0; 2 //@ constraint counter == \old(counter)+1;
二、应用工具链情况
-
OpenJML:以各种神奇的姿势检查JML语法的规范性。
-
JMLUnitNG / JMLUnit:根据规格自动生成测试样例文件,实现自动测试。
-
SMT Solver:检查代码实现对规格的满足程度。
三、JMLUnitNG / JMLUnit
针对Graph接口的实现自动生成测试用例: java -jar jmlunitng.jar -cp specs-homework-2-1.2-raw-jar-with-dependencies.jar src/path/MyGraph.java
JMLUnitNG生成自动测试用例时,对JML注释和代码具有一定的要求,因此可能需要对原有的JML注释进行debug。
可能出现的问题有:
E:\JAVA\project\Homework_3_10.0\src\path\MyPath.java:7: 错误: 已在类 MyPath中定义了变量 nodes //@ public instance model non_null int[] nodes;
代码实现中不能出现与JML注释中相同的变量名。
E:\JAVA\project\Homework_3_10.0\src\path\MyGraph.java:48: 错误: 找不到符号 @ (\exists int i; 0 <= i && i < pidList.length; pidList[i] == pathId);
方法中引入的参数必须与JML注释中出现的参数同名。
E:\JAVA\project\Homework_3_10.0\src\path\MyGraph.java:300: 错误: No modifiers are allowed prior to a lightweight specification case /*@ ensures \result == (\num_of int[] nlist;
对于轻量级方式描述的规格,不能直接放在相应方法之前,可用@override
隔开。
除此之外还可能出现一系列JML语法错误的报错,这个大概是我没有复制粘贴好叭……
命令成功执行后会在MyGraph
所在的目录下生成一系列测试文件,如下图所示:
其中MyPathTest和MyGraphTest由JMLUnit生成,手动填写,其他测试文件由JMLUnitNG自动生成。
这里的MyGraph_JML_Test.java即我们最终要用的测试文件,可在IDEA中打开运行如下:
1 [TestNG] Running: 2 Command line suite 3 4 Failed: racEnabled() 5 Passed: constructor MyGraph() 6 Failed: <<path.MyGraph@de0a01f>>.addEdge(null, -2147483648, -2147483648) 7 Failed: <<path.MyGraph@4c75cab9>>.addEdge(null, 0, -2147483648) 8 Failed: <<path.MyGraph@1ef7fe8e>>.addEdge(null, 2147483647, -2147483648) 9 Failed: <<path.MyGraph@67117f44>>.addEdge(null, -2147483648, 0) 10 Failed: <<path.MyGraph@5d3411d>>.addEdge(null, 0, 0) 11 Failed: <<path.MyGraph@2471cca7>>.addEdge(null, 2147483647, 0) 12 Failed: <<path.MyGraph@5fe5c6f>>.addEdge(null, -2147483648, 2147483647) 13 Failed: <<path.MyGraph@6979e8cb>>.addEdge(null, 0, 2147483647) 14 Failed: <<path.MyGraph@763d9750>>.addEdge(null, 2147483647, 2147483647) 15 Passed: <<path.MyGraph@5c0369c4>>.addNode(-2147483648) 16 Passed: <<path.MyGraph@2be94b0f>>.addNode(0) 17 Passed: <<path.MyGraph@d70c109>>.addNode(2147483647) 18 Passed: <<path.MyGraph@50675690>>.addPath(null) 19 Failed: <<path.MyGraph@31b7dea0>>.addReach(null, -2147483648, -2147483648) 20 Failed: <<path.MyGraph@3ac42916>>.addReach(null, 0, -2147483648) 21 Failed: <<path.MyGraph@2d6a9952>>.addReach(null, 2147483647, -2147483648) 22 Passed: <<path.MyGraph@22a71081>>.addReach(null, -2147483648, 0) 23 Passed: <<path.MyGraph@3930015a>>.addReach(null, 0, 0) 24 Passed: <<path.MyGraph@629f0666>>.addReach(null, 2147483647, 0) 25 Failed: <<path.MyGraph@1bc6a36e>>.addReach(null, -2147483648, 2147483647) 26 Failed: <<path.MyGraph@1ff8b8f>>.addReach(null, 0, 2147483647) 27 Failed: <<path.MyGraph@387c703b>>.addReach(null, 2147483647, 2147483647) 28 Passed: <<path.MyGraph@224aed64>>.containsEdge(-2147483648, -2147483648) 29 Passed: <<path.MyGraph@c39f790>>.containsEdge(0, -2147483648) 30 Passed: <<path.MyGraph@71e7a66b>>.containsEdge(2147483647, -2147483648) 31 Passed: <<path.MyGraph@2ac1fdc4>>.containsEdge(-2147483648, 0) 32 Passed: <<path.MyGraph@5f150435>>.containsEdge(0, 0) 33 Passed: <<path.MyGraph@1c53fd30>>.containsEdge(2147483647, 0) 34 Passed: <<path.MyGraph@50cbc42f>>.containsEdge(-2147483648, 2147483647) 35 Passed: <<path.MyGraph@75412c2f>>.containsEdge(0, 2147483647) 36 Passed: <<path.MyGraph@282ba1e>>.containsEdge(2147483647, 2147483647) 37 Passed: <<path.MyGraph@13b6d03>>.containsNode(-2147483648) 38 Passed: <<path.MyGraph@f5f2bb7>>.containsNode(0) 39 Passed: <<path.MyGraph@73035e27>>.containsNode(2147483647) 40 Passed: <<path.MyGraph@64c64813>>.containsPathId(-2147483648) 41 Passed: <<path.MyGraph@3ecf72fd>>.containsPathId(0) 42 Passed: <<path.MyGraph@483bf400>>.containsPathId(2147483647) 43 Passed: <<path.MyGraph@21a06946>>.containsPath(null) 44 Passed: <<path.MyGraph@77f03bb1>>.getDistinctNodeCount() 45 Passed: <<path.MyGraph@61a52fbd>>.getPathById(-2147483648) 46 Passed: <<path.MyGraph@233c0b17>>.getPathById(0) 47 Passed: <<path.MyGraph@63d4e2ba>>.getPathById(2147483647) 48 Passed: <<path.MyGraph@7bb11784>>.getPathId(null) 49 Passed: <<path.MyGraph@33a10788>>.getShortestPathLength(-2147483648, -2147483648) 50 Passed: <<path.MyGraph@7006c658>>.getShortestPathLength(0, -2147483648) 51 Passed: <<path.MyGraph@34033bd0>>.getShortestPathLength(2147483647, -2147483648) 52 Passed: <<path.MyGraph@47fd17e3>>.getShortestPathLength(-2147483648, 0) 53 Passed: <<path.MyGraph@7cdbc5d3>>.getShortestPathLength(0, 0) 54 Passed: <<path.MyGraph@3aa9e816>>.getShortestPathLength(2147483647, 0) 55 Passed: <<path.MyGraph@17d99928>>.getShortestPathLength(-2147483648, 2147483647) 56 Passed: <<path.MyGraph@3834d63f>>.getShortestPathLength(0, 2147483647) 57 Passed: <<path.MyGraph@1ae369b7>>.getShortestPathLength(2147483647, 2147483647) 58 Passed: <<path.MyGraph@6fffcba5>>.isConnected(-2147483648, -2147483648) 59 Passed: <<path.MyGraph@34340fab>>.isConnected(0, -2147483648) 60 Passed: <<path.MyGraph@2aafb23c>>.isConnected(2147483647, -2147483648) 61 Passed: <<path.MyGraph@2b80d80f>>.isConnected(-2147483648, 0) 62 Passed: <<path.MyGraph@3ab39c39>>.isConnected(0, 0) 63 Passed: <<path.MyGraph@2eee9593>>.isConnected(2147483647, 0) 64 Passed: <<path.MyGraph@7907ec20>>.isConnected(-2147483648, 2147483647) 65 Passed: <<path.MyGraph@546a03af>>.isConnected(0, 2147483647) 66 Passed: <<path.MyGraph@721e0f4f>>.isConnected(2147483647, 2147483647) 67 Failed: <<path.MyGraph@28864e92>>.removeEdge(null, -2147483648, -2147483648) 68 Failed: <<path.MyGraph@6ea6d14e>>.removeEdge(null, 0, -2147483648) 69 Failed: <<path.MyGraph@6ad5c04e>>.removeEdge(null, 2147483647, -2147483648) 70 Failed: <<path.MyGraph@6833ce2c>>.removeEdge(null, -2147483648, 0) 71 Failed: <<path.MyGraph@725bef66>>.removeEdge(null, 0, 0) 72 Failed: <<path.MyGraph@2aaf7cc2>>.removeEdge(null, 2147483647, 0) 73 Failed: <<path.MyGraph@6e3c1e69>>.removeEdge(null, -2147483648, 2147483647) 74 Failed: <<path.MyGraph@1888ff2c>>.removeEdge(null, 0, 2147483647) 75 Failed: <<path.MyGraph@35851384>>.removeEdge(null, 2147483647, 2147483647) 76 Failed: <<path.MyGraph@649d209a>>.removeNode(-2147483648) 77 Failed: <<path.MyGraph@6adca536>>.removeNode(0) 78 Failed: <<path.MyGraph@357246de>>.removeNode(2147483647) 79 Failed: <<path.MyGraph@28f67ac7>>.removePathById(-2147483648) 80 Failed: <<path.MyGraph@256216b3>>.removePathById(0) 81 Failed: <<path.MyGraph@2a18f23c>>.removePathById(2147483647) 82 Failed: <<path.MyGraph@d7b1517>>.removePath(null) 83 Passed: <<path.MyGraph@16c0663d>>.removeReach() 84 Passed: <<path.MyGraph@23223dd8>>.size() 85 86 =============================================== 87 Command line suite 88 Total tests run: 81, Failures: 32, Skips: 0 89 =============================================== 90
由此可见,其自动生成的测试样例均为边界样例。
四、架构设计梳理
总体来说,这三次作业是一个层层递进的关系,通过对上一次作业简单功能的继承,在此基础上增加新的成员变量与方法实现新的、更加复杂的功能。
1.第九次作业
整体架构与类成员变量如下:
本次作业功能较为简单。MyPath主要通过nodes列表保存一个结点序列,通过对其属性和内容的访问来实现相关功能,MyPathContainer则通过两个一一对应的列表paths与ids分别存储路径的结点序列与id,通过nodeMap存储所有节点出现的次数,从而实现增删改查等一系列功能。
对于时间复杂度问题,将所有复杂操作集中在指令数较少的add和remove指令中,其余方法只需要简单的查询相应的数据容器即可得到想要的结果。
2.第十次作业
整体架构与类成员变量如下:
MyPath类与之前相比没有改变。
MyGraph类继承MyPathContainer类中原有的方法,对新增的边相关的方法,通过查询新增的存储边出现次数的成员变量edges实现;对新增的连通相关的方法,通过查询新增的存储连通情况的成员变量reach实现;对新增的最短路径相关的方法,通过查询新增的存储最短路径的成员变量shortest实现。
考虑到时间复杂度的问题,这些新增的成员变量均在add与remove方法中进行更新,其余方法只需查询即可实现。尤其是对于getShortestPathLength方法,对每次计算出的最短路径都进行存储,这样在下一次重复查询时可大大减少所用时间。同时为进一步降低时间复杂度,在MyRailwaysystem类中更改paths与ids的存储容器,从列表改为map,减少了查询与增改的时间开销。
3.第十一次作业
整体架构、更改类的成员变量、方法(规格中规定的除外)如下:
MyPath类与之前相比没有改变。
MyRailwaysystem类中与之前MyGraph类相同的方法部分没有改变,新增的getConnectedBlockCount方法通过引入新的成员变量进行记录,这一变量在add、remove方法中更新联通矩阵的时候同步更新,getLeastTicketPrice、getLeastTransferCount和getLeastUnpleasantValue三个有关带权图最短路径拓展问题的方法通过对Graph类赋予不同的权值间接实现,并未用到推荐的辅助方法。
对于新增的Graph类,用于构建一个带权图并计算其最短路径。这里为实现换乘问题,采用拆点的方法,并对节点重新编号。
五、bug及修复情况
1.第九次作业
-
- getDistinctNodeCount方法时间复杂度太高导致的TLE
通过修改数据结构,增加nodes存储所有节点出现过的次数并在出现次数较少的add和remove类中进行更新即可。
2.第十次作业
-
- getShortestPathLength方法时间复杂度太高导致的TLE
在该方法中增加存储已计算出的点的所有最短路径至shortest的部分,减少二次查询的时间开销即可。
-
- removePath方法返回值错误
在更改paths和ids的容器类型时,对相关方法的修改不彻底,导致返回值产生错误,修改返回值即可。
-
- add方法中更新edges出现错误导致remove产生异常
未考虑同一path中第一次出现一条边后再次出现的情景,导致边数统计出现错误,增加分类讨论即可。
3.第十一次作业
-
- Graph图计算最短路径时结果错误
使用Dijstra方法计算出一系列最短路径进行存储时发生存储失误,订正即可。
不测bug一时爽,一直不测一直爽
六、心得体会
通过JML规格,可以在写代码之前就有一个对于需要实现的类与方法的整体把握,也提高了代码的可读性和规范性。
撰写JML规格,首先对于前置条件,需要全面考虑所有可能出现的情况,分类覆盖讨论并给出相应的处理办法,其次在给出后置条件或抛出异常时,要充分考虑到所有涉及的变量在方法执行前后的差别并依次检查,是一项十分缜密而精细的工作。
根据JML规格实现代码时,应根据给出的前置条件分类处理,在理解其后置条件与异常的前提下选择当前类中合适的方法进行实现,并且在最后确认符合后置条件的要求。