前言
相比于前两个单元,这个单元的代码量小了很多,但是对我们的单元测试能力和对数据结构的理解要求更高了。在完成作业的时候,我们首先需要看懂课程组提供的规格,并按照规格填写函数。完成方法填写之后,我们要学会运用junit来测试我们的程序。
1.JML语言总结
JML(Java Modeling Language)用于描述方法的特征,它可以告诉我们这个方法可以带来什么预期结果,而不去管这个方法具体是如何实现的。这个特征完全符合了面向对象的思想。
理论基础
-
JML表达式
-
原子表达式
\result
\old()
\not_assigned()
\not_modified()
\nonnullelements()
\type()
\typeof()
-
量化表达式
\forall
\exists
\sum
\product
\max
\min
\num_of
-
操作符
子类型关系操作符 <:
等价关系操作符 <==>
推理操作符 ==>
变量应用 \nothing \everything
-
-
方法规格
-
前置条件 reqquires
-
后置条件 ensures
-
副作用范围限定 assignable modifiable
-
signals子句
-
-
类规格
-
状态变化约束 constraint
-
不变式 invariant
-
应用工具链
使用openJML工具,可以测试JML语法正确性、方法静态检查和运行时检查。
JML语法检查
封装好openjml指令后,运行openjml -check Add.java
即可对Add.java文件进行JML语法检测。这方面严格按照JML语法就不会有问题。
静态检测
举一个简单的例子,三次作业中都对数据进行compare处理,如果我们使用减法就会发生溢出问题。
public class Sub { //@ ensures a > b ==> \result > 0; //@ ensures a < b ==> \result < 0; //@ ensures a == b ==> \result == 0; public static int compare(int a, int b) { return a - b; } public static void main(String[] args) { compare(1,2); } }
我们对Sub.java文件进行静态检测,执行
openjml -esc Sub.java
运行结果
Open/Sub.java:6: warning: The prover cannot establish an assertion (ArithmeticOperationRange) in method compare: underflow in int difference return a - b; ^ Open/Sub.java:6: warning: The prover cannot establish an assertion (ArithmeticOperationRange) in method compare: overflow in int difference return a - b; ^ 2 warnings
出现溢出。
2.JMLUnit总结
由于Graph接口过于复杂,我先测试了几个规模小的java文件。沿用上面的Sub.java,使用JMLUnitNG自动生成测试用例执行以下命令
java -jar jmlunitng.jar Open/Sub.java javac -cp jmlunitng.jar Open\*.java java -jar openjml.jar -rac Open/Sub.java java -cp jmlunitng.jar Open/Sub_JML_Test
运行结果
[TestNG] Running: Command line suite Passed: racEnabled() Passed: constructor Sub() Passed: static compare(-2147483648,-2147483648) Passed: static compare(0,-2147483648) Passed: static compare(2147483648,-2147483648) Passed: static compare(-2147483648,0) Passed: static compare(0,0) Passed: static compare(2147483648,0) Passed: static compare(-2147483648,2147483648) Passed: static compare(0,2147483648) Passed: static compare(2147483648,2147483648) -1 Passed: static main(null) Passed: static main({}) =============================================== Command line suite Total tests run: 12, Failures: 3, Skips: 0 ===============================================
3.作业分析
这三次作业是一个不断添加新功能的单元,分为Path
, PathContainer
, Graph
和RailwaySystem
这四部分。
第一次作业
在Path类中,我使用ArrayList来存储单个Path,使用HashSet来存储点集。
HashMap增删非常容易,并且查询时的复杂度仅为O(1)。本次作业最复杂的是getDistinctNodeCount(),但是实际上只需要返回set.size即可。
第二次作业
第二次作业多了联通性及最短路径的查找,这就需要我们构建一个图。我采用邻接矩阵的方法来保存这个图。在计算最短最短路径的时候,我选择Floyd算法一次计算出所有点之间的最短路径,这样只需要在增删点的时候重新构图并计算最短路径即可。
在初始化邻接矩阵时,会遇到这种情况:需要知道所有点中的第i个点的nodeId;需要知道nodeId是所有点中的第几个点。所以我设置一个ArrayList来设置点和下标的转换。我们需要在增删path的时候维护这个list。
第三次作业
第三次作业增加了基于地铁图的换乘、票价、不满意度,所以可以基于权重来计算最短路径。
总结
三次作业都可以用Junit来压力测试,但是只能判断时间复杂度是否符合要求,并不能判断结果是否正确。这里放上一个很简单的例子
运行结果
可以看出270000条指令可以在2秒内跑完。
4.BUG修复
三次作业如果严格按照规格要求来完成,基本不会有问题。我在第二次作业的时候,取最短路径的时候,两个相同点的最短路径我求出的是1,最后发现是相同点特判问题。因为这个问题我强测错了1个点,互测被hack了6次。
5.心得体会
对JML的思考
虽然我很认真地学习了JML规格,但是实际上写规格地时候还是有些困难。但是在阅读jml方面,我意识到了数学语言、逻辑语言地规范性和准确性。
在撰写规格方面,
总结