JML以javadoc注释的方式来表示规格,每行都以@起头。java
//@annotation
/* @ annotation @*/
JML的表达式是对Java表达式的扩展,新增了一些操做符和原子表达式。算法
expr
)表达式:用来表示一个表达式expr
在相应方法执行前的取值。针对一个对象引用而言,只能判断引用自己是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。true
,不然返回false
。container
)表达式:表示container
对象中存储的对象不会有 null。type
)表达式:返回类型type
对应的类型(Class)。expr
)表达式:该表达式返回expr
对应的准确类型。E1<:E2
,若是类型E1是类型E2的子类型(sub type),则该表达式的结果为真,不然为假。若是E1和E2是相同的类型,该表达式的结果也为真。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
。对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为true
。assignble
表示可赋值。modifiable
则表示可修改。signals (Exception e) b_expr
:当b_expr
为true
时,方法会抛出括号中给出signals_only
:后面跟着一个异常类型,不强调对象状态条件,强调知足前置条件时抛出相应的异常。一开始想对Path中的一些简单方法进行测试,可是报了很奇怪的错误,也不懂如何解决,遂放弃。编程
因而我手写了一个简单的测试程序Test.java,其功能是非负数的加法,且未对溢出状况作处理。缓存
package test; public class Test { //@ public normal_behavior //@ requires a >= 0 && b >= 0; //@ ensures \result == a + b; public static int sum(int a, int b) { return a + b; } public static void main(String[] args) { sum(1, 2); } }
初始目录结构以下:架构
test └── Test.java
执行java -jar jmlunitng.jar test/Test.java
ide
test ├── PackageStrategy_int.java ├── PackageStrategy_java_lang_String.java ├── PackageStrategy_java_lang_String1DArray.java ├── Test.java ├── Test_InstanceStrategy.java ├── Test_JML_Data │ ├── ClassStrategy_int.java │ ├── ClassStrategy_java_lang_String.java │ ├── ClassStrategy_java_lang_String1DArray.java │ ├── main__String1DArray_args__10__args.java │ ├── sum__int_a__int_b__0__a.java │ └── sum__int_a__int_b__0__b.java └── Test_JML_Test.java
执行javac -cp jmlunitng.jar test/*.java
工具
test ├── PackageStrategy_int.class ├── PackageStrategy_int.java ├── PackageStrategy_java_lang_String.class ├── PackageStrategy_java_lang_String.java ├── PackageStrategy_java_lang_String1DArray.class ├── PackageStrategy_java_lang_String1DArray.java ├── Test.class ├── Test.java ├── Test_InstanceStrategy.class ├── Test_InstanceStrategy.java ├── Test_JML_Data │ ├── ClassStrategy_int.class │ ├── ClassStrategy_int.java │ ├── ClassStrategy_java_lang_String.java │ ├── ClassStrategy_java_lang_String1DArray.class │ ├── ClassStrategy_java_lang_String1DArray.java │ ├── main__String1DArray_args__10__args.class │ ├── main__String1DArray_args__10__args.java │ ├── sum__int_a__int_b__0__a.class │ ├── sum__int_a__int_b__0__a.java │ ├── sum__int_a__int_b__0__b.class │ └── sum__int_a__int_b__0__b.java ├── Test_JML_Test.class └── Test_JML_Test.java
执行java -jar openjml.jar -rac test/Test.java
post
执行java -cp jmlunitng.jar test.Test_JML_Test
学习
测试结果:测试
[TestNG] Running: Command line suite Passed: racEnabled() Passed: constructor Test() Passed: static main(null) Failed: static sum(-2147483648, -2147483648) Passed: static sum(0, -2147483648) Passed: static sum(2147483647, -2147483648) Passed: static sum(-2147483648, 0) Passed: static sum(0, 0) Passed: static sum(2147483647, 0) Passed: static sum(-2147483648, 2147483647) Passed: static sum(0, 2147483647) Failed: static sum(2147483647, 2147483647) =============================================== Command line suite Total tests run: 12, Failures: 2, Skips: 0 ===============================================
能够看到自动生成的测试用例采用的是极端数据的组合,对于负数以及溢出都显示Failed代表未经过测试,这与咱们的预期相符。
第一次做业比较简单,只有对路径的增删查改等基本功能,仅需实现Path
和PathContainer
两个容器类再加上一个主类便可,实现的时候根据JML循序渐进地写就没什么问题。惟一要注意的一点是时间复杂度的问题,由于查询指令不少,使用HashMap
和HashSet
是一个较好的选择,能基本保证O(1)的复杂度。
从此次做业开始涉及到图结构,增长了判断容器中是否存在某个结点、容器中是否存在某一条边、两个结点是否连通以及计算两个结点之间的最短路径的方法。
对于结点我使用HashMap
存储,以结点Id为值,重复个数为键。对于边我采用的是嵌套的HashMap
,由结点再映射到一个HashMap
,内容是与它链接的结点及其重复个数。这样,就能把图结构完整的保存下来,查询效率高,同时也易于增删维护。
对于连通性和最短路,我采用了bfs,遍历的过程当中会用到一个WeightedNode
类,用来保存源点到当前节点的最短路径长度,并传递给下一个节点累加使用。此外,我还用ShortestPath
类来描述已经算出的最短路,它包含两个节点的信息,并重写了equals()
和hashCode()
,从而能够保存在HashMap
中做为最短路的缓存。值得一提的是,a -> b
和b -> a
的最短路是同样的,在重写以上两个方法时要注意对称性。
本次做业须要实现一个动态的地铁系统。从类图中的继承关系能够看出,这三次做业是一脉相承、逐次递进的,模拟了实际OOP开发中一个功能模块的演化过程。
在保留了上次做业的大致架构的基础上,引入MultiNode
来描述在不一样路径上具备相同Id的结点,这是由于我采用的是"拆点"的建图方法,须要区分这些重复的结点。此外,用Pair
类代替并扩展了ShortestPath
类,使其能够同时描述最短路路径、最低票价、最少换乘次数、最少不满意度多种两点结构。算法上采用Dijkstra算法,在每次查询时计算出源点到其所在连通块的全部节点的最低票价/最少换乘次数/最少不满意度,并存入缓存以便下次直接使用。至于最短路和连通块,依然是用bfs进行计算。
本次做业主要有两方面的不足:
三次做业均用对拍进行测试。
多是由于比较简单,没有被测出bug,也没有测出别人的bug。
依然没有测出或被测出bug。
提交前就在担忧会不会由于拆点复杂度太高而超时,结果果真惨不忍睹,未经过的点都是由于TLE。目前正在bug修复阶段,考虑换一种建图的方法。
本单元主要学习JML规格,具体来讲包含两方面的内容:根据需求撰写规格,以及根据规格实现代码。JML是基于"契约式编程"的一种规格描述语言,相比于天然语言注释,JML更加严谨和清晰。只要能保证规格自己是知足需求的,而且编程时严格按照规格实现,理论上就程序就必定是正确的。在这种状况下,即便出现了bug,也能经过OpenJML、JMLUnitNG等工具自动化地定位问题所在。
但JML也有美中不足的地方,好比学习成本高,读起来没有天然语言那么易于理解。尤为是撰写规格是一件极其费时费力的工做,其难度不亚于代码实现自己。可能在工业界,尤为是那些不允许任何程序错误的场景下(如航空航天、军事领域),使用JML是一种较好的易于沟通和协做的编程方式,且能在最大程度上避免错误的产生。但在小团队的常规开发中,私觉得天然语言会是相对更好的选择。
然而不管如何,JML是一门值得了解和学习的技术。