OO第三单元作业总结
本单元的三次作业根据所给的Jml
规格完成代码,依次实现Path
路径类,PathContainer
路径容器类;第二次作业将PathContainer
类扩展为Graph
类,整合图数据结构功能,解决部分图的问题;第三次作业将Graph
类扩展为RailwaySystem
类,完成更复杂的图功能。
JML语言理论基础
-
JML(Java Modeling Language)是一种行为接口的规范语言,通过规格所规定的
-
前置条件
-
副作用范围限定
-
后置条件
来对设计的行为进行约束,准确表达方法的功能需求。同样,通过JML在x形式规范的基础上,可以利用第三方工具来进行高效的单元测试,即基于正确规格的程序就可以被认为是正确的程序 .
-
-
原子表达式
-
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
-
\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
-
-
量化表达式
-
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束
-
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
-
-
操作符
-
子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。
-
等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是 b_expr1==b_expr2 或者 b_expr1!=b_expr2 。
-
推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1 。
-
-
类型规格
-
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中 invariant 为关键词, P 为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。
-
状态变化约束(constraint)对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式
-
openJML
使用-check
选项可以检查JML语法的正确性,同时也可以不依赖JML,静态验证方法可能存在的问题,或是生成运行时检查的class
文件。JMLUnitNG
根据-rac
运行时检查选项,可以生成一个Java类文件测试的框架,实现对代码的自动化测试。这种自动化测试对于方法的边界测试比较支持。
部署SMT Solver
在讨论区大佬的帮助下,对OpenJML进行了简单的下载安装以及使用,接着对几个方法进行尝试验证。
package demo; import java.util.ArrayList; public class Path { /* @ requires nodes != null; @ ensures \result == (\num_of int i, j; 0 <= i && i < j && j < nodes.length;nodes[i] != nodes[j]); @*/ public /*pure*/ static int getDistinctNodeCount(int[] nodes) { int len = nodes.length; for (int i = 0;i < nodes.length;i++) { for (int j = i + 1;j < nodes.length;j++) { if (nodes[i] == nodes[j]) { len--; break; } } } return len; } /* @ ensures \result == (a + b); @ */ public static int addNode (int a,int b) { return a + b; } public static void main(String[] args) {} }
使用./openjml.sh -check demo/Path.java
对JML语法进行检查,没有发现错误。
使用 ./openjml.sh -exec Java/openjml/win/Solvers-windows/z3-4.7.1.exe -esc demo/Pat h.java
对方法静态检查
$ ./openjml.sh -exec Java/openjml/win/Solvers-windows/z3-4.7.1.exe -esc demo/Path.java demo\Path.java:25: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method addNode: underflow in int sum return a + b; ^ demo\Path.java:25: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method addNode: overflow in int sum return a + b; ^ demo\Path.java:13: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method getDistinctNodeCount if (nodes[i] == nodes[j]) { ^ demo\Path.java:13: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method getDistinctNodeCount if (nodes[i] == nodes[j]) { ^ demo\Path.java:14: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method getDistinctNodeCount: underflow in int difference len--; ^ 5 个警告
发现getDistinctNode
方法中可能存在的错误下标以及len可能溢出的问题,其实我不是很懂为什么会有这两个问题以及add
方法可能存在的溢出问题。将方法进行修改后,使用JMLUnitNG自动化生成测试样例
john@DESKTOP-TI0P349 MINGW64 /d $ java -jar jmlunitng.jar "$@" demo/Path.java $ ./openjml.sh -rac demo/Path.java $ javac -cp jmlunitng.jar demo/*.java $ java -cp jmlunitng.jar demo.Path_JML_Test [TestNG] Running: Command line suite Failed: racEnabled() Passed: constructor Path() Passed: static addNode(-2147483648, -2147483648) Passed: static addNode(0, -2147483648) Passed: static addNode(2147483647, -2147483648) Passed: static addNode(-2147483648, 0) Passed: static addNode(0, 0) Passed: static addNode(2147483647, 0) Passed: static addNode(-2147483648, 2147483647) Passed: static addNode(0, 2147483647) Passed: static addNode(2147483647, 2147483647) Failed: static getDistinctNodeCount(null) Passed: static getDistinctNodeCount({}) Passed: static main(null) =============================================== Command line suite Total tests run: 14, Failures: 2, Skips: 0 ===============================================
由于不支持exist和forall
,因此所能测试的方法极其有限。除此之外,我测试了多个简单的方法,JMLUnitNG自动化生成的样例好像比较难以描述,只会生成一些最大最小空以及Null的测试样例,不知道是不是我的打开方式不对生成的测试代码过于单一,没有太大的价值。
架构分析
三次作业我只截了第三次的类图,虽然这三次作业层层递进,但我每次只是简单的将需要实现的类进行实现,因此毫无架构可言,简直就是堆叠的一座垃圾山。从第一次实现的MyPathContainer
到MyGraph
再到MyRailwaySystem
,当有新的需求产生时我只是简单的将功能叠加在新增加的类中,因此可以看到新增的类十分臃肿,图结构以及对于需求的计算都堆叠在这个类中,如果有新的需求需要增加,那么可能很多类都无法幸免需要修改。
直到我看到了例程,我才意识到架构的美妙。但是自己实现过程中总是忽略架构,想不到架构,只是将任务完成将需求满足例程中将核心图类进行单独封装,实现图的基本功能,增删路径等,然后引出有向图和无向图;缓存计算层,基于图类实现图的l两个核心计算功能。最后图建模层使用了组合模式+工厂模式,将多个问题归为一种问题,需要求解某个问题时直接调用内部的缓存计算模块,进行计算。
在实现了架构以后,如果有新的功能需要实现,便无需或很少的改动已经实现的代码,相比于我的一座垃圾山,真是赞叹不已获益匪浅。
BUG分析
第一次作业在强测中遭遇到TLE和WA双重打击。首先第一次作业中没有意识到复杂度的重要性,每次在进行不同结点个数的计算时,保留上次计算结果,如果之前改变过就重新计算,但还是炸掉了。通过使用HashMap将不同节点进行储存,查询时直接输出size()解决。
其次WA是因为在判断结点不同时,错误的将两个Integer使用==进行了比较,简直愚蠢至极。由于测试不充分,只是进行了较小结点的测试(小于127),因此没有发现这个bug,导致大量WA。
第二次第三次作业中没有出现bug。
心得体会
对于JML规格,其目的在于对一个方法或是类进行描述,来保证其严格性和准确性。在体会到JML的目的后,如果JML规格已经写好,那JML的使用理解大概就是一个参考书,先看一遍对方法的要求有一个大致的印象,然后再去实现方法。如果有哪里模糊则再去查询。