BUAA_OO_第三单元
单元任务
本单元任务分为三个阶段,均为根据JML规格完成具有一定功能的代码。依次为:
-
实现两个容器类Path和PathContainer,对JML规格进行理解和熟悉;
-
实现容器类Path和数据结构类Graph,其中Graph类继承了PathContainer接口,对JML规格进一步理解;
-
实现容器类Path,地铁系统类RailwaySystem,其中RailwaySystem类继承了Graph接口。
一、JML语言
JML是java modeling language的缩写,用于对Java程序进行规格化设计的一种表示语言,可以用来描述一段代码的具体行为,比如前置条件、副作用、后置条件等。
通过JML的相关支持工具,可以检查规格是否合乎规范、可以基于规格自动构造测试用例,同时可使用SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
(一)JML理论基础
JML中存在大量对Java程序中数据、方法、类的描述,并以Java语言中注释的形式嵌入到程序中,不会影响正常的编译而能够规范代码的使用,而能够精确地描述代码。
主要的规格有:
JML表达式
-
\result表达式:方法执行后的返回值。
-
\old( expr )表达式:用来表示一个表达式expr 在相应方法执行前的取值。
-
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
-
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
-
\sum表达式:返回给定范围内的表达式的和。
-
\max,\min表达式:返回给定范围内的表达式的最大值、最小值。
-
等价关系操作符: b_expr1<==>b_expr2 或者b_expr1<=!=>b_expr2 分别表示表达式相等或不等。
-
推理操作符: b_expr1 = => b_expr2 或者b_expr2 <==b_expr1 。
方法规格
-
前置条件:requires P1||P2; 对方法输入参数的限制。
-
后置条件:ensures P; 对方法执行结果的限制。
-
副作用范围限定: assignable 可赋值 modifiable 可修改
-
pure方法:纯粹访问性的方法,即不会对对象的状态进行任何改变,也不需要提供输入参数。
-
方法的异常行为:normal_behavior, also, exceptional_behavior, signals (***Exception e) b_expr, signals_only。
类型规格
-
不变式invariant,要求在所有可见状态下都必须满足的特性。
-
状态变化约束constraint,来对前序可见状态和当前可见状态的关系进行约束。
(二)JML应用工具链情况
JML编译器,比如OpenJML,编译含有JML标记的代码。
-
-check 选项可以对生成的类文件进行JML规范检查,检查是否含有语法错误;
-
-esc 选项可以对程序代码进行静态检查,检查程序中的潜在问题;
-
-rac 选项可以对程序代码进行运行时检查;
JMLdoc,与javadoc工具相似,不同的是它在生成的Html格式文档中包含JML规范;
JMLUnitNG,可以根据JML生成一个Java类文件测试的框架,结合OpenJML的-rac运行时检查选项,实现对代码的自动化测试。
二、部署SMT Solver
SMT(Staisfiability modulo theories) Solver即定理证明器。开源工具包OpenJML中已经包含了CVC4、Z3方案。
根据讨论区的OpenJML基本使用部署或者直接下载安装包,java -jar openjml.jar
因为给出的规格不能完全支持自己后期修改补充的程序,所以可能要补充规格使其能够与自己的程序相匹配。
1.MyPath 中的equals
方法
MyPath.java:46: warning: The prover cannot establish an assertion (ExceptionalPostcondition: /usr/share/java/openjml/openjml.jar(specs/java/lang/Object.jml):76: ) in method equals for (int i = 0; i < this.size(); i++) {
警告的原因是this可能不存在,故不存在this.size(),对于这种情况程序可能会出错。因此应对可能出错的情况加上try...catch
try { thisSize = this.size(); } catch (Exception e) {}
2.MyGraph中的addPath
方法
MyGraph.java:188: warning: The prover cannot establish an assertion (ArithmeticOperationRange) in method addPath: overflow in int sum id++; ^
警告原因为id自加后可能会超出int的范围,在实际应用中,应该用容量更大的数据类型或者对id的值加以判断以防越界,对于本单元的作业,保证了使用int类型的安全性,故没有加以判断。
3.MyGraph中的floyed
方法
使用OpenJML对程序进行静态分析后,在该方法中部分报错如下:
MyGraph.java:118: warning: The prover cannot establish an assertion (UndefinedTooLargeIndex) in method floyed dist[i][j] = temp; ^ MyGraph.java:118: warning: The prover cannot establish an assertion (UndefinedNegativeIndex) in method floyed dist[i][j] = temp; ^ MyGraph.java:106: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method floyed System.arraycopy(matrix[i], 0, dist[i], 0, lens); ^ MyGraph.java:106: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method floyed System.arraycopy(matrix[i], 0, dist[i], 0, lens); ^ MyGraph.java:112: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method floyed if (dist[i][k] == maxInt || dist[k][j] == maxInt) {
这些报错基本上都是因为没有对静态数组的索引进行限制和判断造成的。由此可以看出静态数组不应该出现在大型的项目中,因为静态数组存在极大的隐患。
4.MyGraph中的getShortestPathLength
方法和isConnected
方法
MyGraph.java:68: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method getShortestPathLength return dist[nodeinf.indexOf(fromNodeId)][nodeinf.indexOf(toNodeId)]; ^ MyGraph.java:68: warning: The prover cannot establish an assertion (PossiblyNullDeReference) in method getShortestPathLength return dist[nodeinf.indexOf(fromNodeId)][nodeinf.indexOf(toNodeId)]; ^ MyGraph.java:68: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method getShortestPathLength return dist[nodeinf.indexOf(fromNodeId)][nodeinf.indexOf(toNodeId)]; ^ MyGraph.java:68: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method getShortestPathLength return dist[nodeinf.indexOf(fromNodeId)][nodeinf.indexOf(toNodeId)]; ^ MyGraph.java:68: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method getShortestPathLength return dist[nodeinf.indexOf(fromNodeId)][nodeinf.indexOf(toNodeId)]; ^ MyGraph.java:55: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method isConnected return dist[fm][to] != maxInt; ^ MyGraph.java:55: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method isConnected return dist[fm][to] != maxInt; ^ MyGraph.java:55: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method isConnected return dist[fm][to] != maxInt; ^ MyGraph.java:55: warning: The prover cannot establish an assertion (PossiblyNullDeReference) in method isConnected return dist[fm][to] != maxInt; ^ MyGraph.java:55: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method isConnected return dist[fm][to] != maxInt; ^
同样也是因为静态数组的索引可能越上界、越下界。
三、部署JMLUnitNG/JMLUnit
JMLUnitNG是JMLUnit的一个基于TestNG的继承者,是一个用于JML注释的Java代码的自动化单元测试生成工具,它使用JML断言作为测试oracles。
使用如下语句生成针对Graph接口的测试用例:
java -jar jmlunitng.jar -cp specs-homework-2-1.2-raw-jar-with-dependencies.jar MyGraph.java
以下为生成的测试文件,其中:
MyGraph_JML_Test.java
- 包含所有生成的TestNG测试的类。此类有一个main
运行测试的 方法,也可以在运行TestNG测试所支持的任何方法中与TestNG一起使用。MyGraph_InstanceStrategy.java
- 用于生成类实例的策略类 MyGraph。PackageStrategy_int.java
- 类型的包级数据策略int
。int
添加到此文件的值将用作int
包中每个类的每个方法的每个参数的输入P
。PackageStrategy_com_oocourse_specs2_models_Path.java
- 类型的包级数据策略。MyGraph_JML_Data
- 测试数据目录,内包含类级的策略以及针对每个方法的策略。
MyGraph_JML_Data文件夹:
由于进一步的编译报错,因此采用简单样例进行进一步尝试
java -jar jmlunitng.jar dd/Demo.java javac -cp jmlunitng.jar dd/**/*.java dd/*.java java -cp jmlunitng.jar: dd.Demo_JML_Test
运行结果如下:
[TestNG] Running: Command line suite Passed: racEnabled() Passed: constructor Demo() Passed: static compare(-2147483648, -2147483648) Failed: static compare(0, -2147483648) Failed: static compare(2147483647, -2147483648) Passed: static compare(-2147483648, 0) Passed: static compare(0, 0) Passed: static compare(2147483647, 0) Failed: static compare(-2147483648, 2147483647) Passed: static compare(0, 2147483647) Passed: static compare(2147483647, 2147483647) Passed: static main(null) Passed: static main({}) =============================================== Command line suite Total tests run: 13, Failures: 3, Skips: 0 ===============================================
可以看出该种测试方法主要适用于边界检查。生成的测试用例的一般方法为:
-
对于int,生成0,-2147483648,2147483647,排列组合进行测试
-
对于Object的子类,生成null及自身类型的空集进行测试。
四、架构设计
1. 第一次作业
本次作业架构约等于没有架构,只是完成了需要完成的两个类而已。
对于Path类
-
使用 ArrayList<Integer> 结构来存结点,类中涉及的方法,直接根据JML来写或者简单调用 ArrayList 本身的功能,如 iterator() 和 hashCode() 等。
-
使用 diff 来统计计算路径中不同的结点个数,若 diff 不为0,那么直接返回不需要重新计算。
对于PathContainer类
-
使用HashMap<Integer, Path> 结构完成根据索引查找路径。
-
使用HashMap<Path, Integer> 结构来完成根据路径查找索引。
-
使用HashMap<Integer, Integer> 结构完成对当前容器内所有路径不同结点的查询,第一个 Integer 代表结点值,第二个 Integer 代表该结点在容器内的数目。每次增加路径时,该路径中结点对应的个数增加;每次删除路径的时候,将该路径中的每个结点对应的值减1,当出现次数变为0时在HashMap中 删去该结点。每次查询只需返回 key 的数量。
2. 第二次作业
本次作业依然没有什么架构,原本可以继承上一次作业的PathContainer,但是图省事直接 Ctrl+C 并 Ctrl+V 到本次作业,并加了本次需要的方法。同时本次Path类相比于上次没有什么变化。
对于Graph类
-
其中的matrix,dist,nodeinf 是为了 Floyed 算法准备。matric为固定长度122×122的静态数组,用于初始化邻接矩阵;dist为每次长度根据当前节点个数所定的静态数组,长度不定是为了在节点个数较少时减少计算次数;nodeinf为 ArrayList<Integer> 结构提供了从不同结点到 0-n的映射,将原本可能的巨大的稀疏图缩到最大为120×120的图。
-
使用 HashMap<Integer, HashSet<Integer>> 结构存储图,可看做一个邻接表。在判断图中是否存在该边时,找到 fromNodeId 结点对应的 HashSet,查找 toNodeId 结点是否在其中,若包含则存在两点相连的边。
-
是否相连与最短路径,直接从通过 Floyed 算法算出的数组中判断,若dist[fm,to] 所存的值为有效值,那么查询的点可达,进一步可知所存的值为两结点的最短路径。
3. 第三次作业
本次作业有点OO的样子了,其中:
-
MyPath类与MyPathContainer类比起第一次作业,没有什么变化;
-
MyRailwaySystem类继承自MyPathContainer类,并增加了RailwaySystem接口中要求的方法;
-
将上一次作业中用到的Floyed算法抽象到Shortest类和Floyed类,供多次使用;
-
Price类、Unpleasant类、Transfer类继承自Shortest类。
总的来说本次作业中每个类各司其职,虽然没有对算法进行优化,但是结构和时间复杂度都还算满意。
对于Blocks类
-
即并差集,相对于其他类来说比较独立。每次添加路径时不需要重新计算,只需要更新将路径更新进来,内部存储的结点映射关系和其他类不同;删除路径时,重新建一个新类,将剩余路径一一加入其中。
对于Shortest类
主要函数有:
public int[][] getDist() { return floyed.getDist(); } private void init(int[][] mat) { for (int i = 0; i < nodemax; i++) { for (int j = i; j < nodemax; j++) { mat[j][i] = maxInt; mat[i][j] = maxInt; } mat[i][i] = 0; } } public void refleshNode(int temp, int keep) {} public void refleshPath(Path path, int type) {} public void count() { floyed = new Floyed(nodeinf, matrix); } public int getShortest(int fromNodeId, int toNodeId) { return floyed.getShortest(fromNodeId, toNodeId); }
在其他继承的类中,只需要改 refleshPath 方法 和 getShortest 方法即可。
算法是使用的讨论区中不拆点的方法,每次增删节点都重新计算一遍。
五、出现Bug及修复情况
1.第一次作业
由于将对结点、路径的操作均摊到次数较少的添加删除操作中,同时选择的数据结构也能够保证查询时时间复杂度低,且在本地使用较为复杂的数据进行测试,时间也在5s以内。
本以为万无一失,结果发现测评机测评时间和本地运行有较大的误差,出现了TLE的情况。
之后发现出现Bug的原因是在对路径中节点的操作里,没有使用迭代器,而是使用了循环遍历多次调用函数的方法,如方法:
private void addNode(Path path) { for (int i = 0; i < path.size(); i++) { int node = path.getNode(i); if (nodes.containsKey(node)) { nodes.put(node, nodes.get(node) + 1); } else { nodes.put(node, 1); } } }
方法中遍历路径中的每个节点,是将索引 i 遍历路径中节点个数,再在路径中查找索引对应的结点,将以上方法改为如下的迭代器方法之后,速度快了4倍。
private void addNode(Path path) { for (int node : path){ if (nodeset.containsKey(node)) { nodeset.put(node, nodeset.get(node) + 1); } else { nodeset.put(node, 1); } } }
2.第二次作业
对于Floyed存储路径的初始化如图所示,对角线代表结点到自己的距离,设为0。
但是在后期添加路径的时候,对于路径1 2 2 3 而言,结点2与2直接添加了一条路径,此时路径长度变为了1,也就是说自环改变了到自身的距离。
对于这个Bug,有两种解决方法,一种是在添加完边之后,再把每个对角线设为0;另一种方法是添加边的时候判断该边已存在的边是否比要添加的边小,如果是的话就不再更新边。
3.第三次作业
本次作业没有Bug。自己测试时应该注意没有结点的情况、查询结点自身到自身的情况。
六、规格撰写和理解的心得体会
代码规格能够对对代码的逻辑、风格进行一定的规范,一方面提前写好规格保证其逻辑的正确,就可以让团队里的每个人分别实现一定的功能,避免了每个人有自己的逻辑导致的代码重复、接口不统一再次翻工等问题,同时优化了代码风格;另一方面规格比起代码来说总是更短一点的,逻辑清晰的规格增加了易读性,能够便于代码的维护。
对于规格的撰写,需要注意到的一点是规格限制的是方法的结果,对于方法的具体实现并不作出限制。这样的话对于非团队的小项目,就不需要考虑“应该先写规格还是先写方法”的问题了,直接写完规格,照着比较结果即可。否则如果规格和方法必须一一对应,代码实现的过程中总会遇到各种各样的小问题,还要考虑现实需要,要改变实现过程,那么还要反过来修改规格,无疑增大了工作量。
我们本单元中只对给出的代码写简单的JML,或者完全根据JML补充方法代码,并没有撰写大项目的代码规格。但未来参加大型项目需要分工完成时,还是会需要这个能力的,需要不断进行锻炼。