【OO学习】OO第三单元作业总结
第三单元,我们学习了JML语言,用来进行形式化设计。本单元包括三次作业,通过给定的JML来实行了一个对路径的管理系统,最后完成了一个地铁系统,来管理不同的线路,求得关于价格、换乘、不满意度等最短路信息。
本文将介绍:JML语言理论基础、应用工具链和工具的使用方法;SMT Solver;JMLUnitNG自动生成测试用例;三次作业的架构设计;Bug查找策略;个人心得。
JML
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML主要由Leavens教授在Larch上的工作,并融入了Betrand Meyer, John Guttag等人关于Design by Contract的研究成果。近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
一般而言,JML有两种主要的用法:
- 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
- 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
一般JML规格中,会描述 requires先验条件,ensures后验条件,assignable可能改变的内容,exceptional_behavior异常抛出条件。对类的规格描述有invariant不变式,constraint变化约束。
可以使用开源的openJML工具来进行对JML规格进行原语检查、静态验证以及动态验证。
原语检查
由于已经有很多同学讲解了在Linux下的使用方法,我这里主要说下在Windows下的使用。
首先把所有需要测试或相关的java文件路径写到一个.txt中。可以使用windows下dir指令将目录下所有java文件写到java.txt中。
1 dir *.java /s /b > java.txt
之后就可以使用openjml来检查JML规格是否书写正确。
1 java -jar yourPath\openjml.jar -exec yourPath\Solvers-windows\z3-4.7.1.exe -check @java.txt -encoding UTF-8
注意需要使用-encoding UTF-8,否则中文编码会报错。
静态验证
1 java -jar yourPath\openjml.jar -exec yourPath\Solvers-windows\z3-4.7.1.exe -esc @java.txt -encoding UTF-8
在使用静态验证的时候可能报错:
根据提示,需要加上-exec 来指定使用的SMT solver的版本。
动态验证
1 java -jar yourPath\openjml.jar -exec yourPath\Solvers-windows\z3-4.7.1.exe -rac @java.txt -encoding UTF-8 2 java -cp jmlruntime.jar; Main
这里要注意的是,使用-cp的时候包后边加上分号,否则也会有报错。 :)
SMT Solver
SMT( Satisfiability modulo theories )求解器在形式化方法、程序语言、软件工程、以及计算机安全、计算机系统等领域得到了广泛应用。
JMLUnitNG
关于对MyGraph类使用JMLUnitNG生成测试用例。
首先需要将相关类中的中文注释全部删除(com.oocourse.space2.models内的所有文件),否则会出现编码错误。
1 java -jar E:\workspace\OO\openJML\jmlunitng-1_4.jar java\MyGraph.java java\com\oocourse\specs2\models\*.java
使用如上指令进行测试用例生成。
如果出现关于HashMap的报错,需要将new HashMap中的模板补全。
生成测试用例后,可以通过Idea运行。
我的结果中有出现了很多“错误”,根据测试信息,大概发现是因为很多测试都是边缘测试,测了Int的极值,可以检测出,但是由于返回了异常,所以会报错。
可以看出,JMLUnitNG生成的单元测试,比较关注于数据边界,可以用来提醒程序员考虑数据边界。但是对于基本简单的测试,还是需要手动生成。
三次作业架构设计
第一次作业
对Path接口的实现,主要按照JML来设计。用ArrayList来记录node顺序。为了加速求DistinctNode个数,用了Hashmap来存储每个节点出现的次数。
对PathContainer接口的实现,我用了Hashmap来将每条Path的编号与Path本身做映射。在查找Path对应的Id时,直接使用了枚举操作(Path的个数不会很多)。同样为了加速求DistinctNode个数,用了Hashmap存储了每个节点出现次数,在addPath和removePath时维护Hashmap的内容。
第二次作业
和上次作业相比,这次作业出现了图的概念,需要维护节点、边和最短路。由于添加、删除Path的次数很少,因此这次作业采用了在每次加减Path的时候,重新建立相关数据,然后查询的时候直接O(1)查询。用二维数组,记录边和最短路的信息。最短路用floyd算法求解。
Path接口的实现与第一次作业一样。
Graph接口的实现,我是直接在上次PathContainer的基础上添加了一些属性和方法。首先对getPathId方法,我新建了一个Hashmap,建立了Path和PathId之间的映射。
因为点是离散的,我是用Hashmap将点进行了重新编号,把原本分布于Int范围内的点,映射到1~n之间,方便计算和存储。
对图的维护,每当有修改路径时,我都重新对点编号,构建最短路矩阵和连边矩阵。
对图的查询操作比较简单,直接从相关容器中查询就行。
第三次作业
在这次作业之前,我对第二次作业进行了分析,发现Graph类的功能过于庞大。因此这次的作业采用了继承机制。每个接口都对应一个具体类,类的继承关系和接口的继承关系相同。这样每个类的都只实现对应接口的功能。具体类图如下:
对于4种最短路的实现,采用了Floyd来一次性计算,然后O(1)查询。建图方法参考了讨论区wjy的做法(wjynb)。关于她的建图方法,我的理解是每条边<u, v>表示,从u走到v然后换乘。所以最后要减去一次换乘的代价。这样可以避免因为拆点处理换乘带来的点过多(120 -> 5000)导致复杂度增加。
为了方便维护图,所有关于图的操作我都放到Graph类(非MyGraph)里,里面实现了加边、计算、查询这些方法。关于图连通性的维护也在这里,使用了染色算法,dfs查询后存储下来,O(1)查询。
MyGraph类内部,主要是实现了当增加和删除Path时,重构graph(Graph类的实例)。由于按照wjy的做法,需要计算出每条Path内部的最短路,因此我实现了PathGraph类,来记录已添加的Path内部最短路,这样可以减少重复计算带来的时间损耗。
MyRailwaySystem中,主要是参考JML规格跑出异常,然后调用graph内的方法来查询就行。
Bug查找策略
这三次作业我分别采用了不同的测试方法。
第一次作业
我使用了JUnit来进行单元测试,对Path和PathContainer的每个方法都进行了测试。同时我也使用JUnit做了压力测试,添加了很多Path,然后多次查询,最终发现了DistinctNode会超时。
第二次作业
第二次作业我也使用了JUnit,和上次作业类似。
我还使用了JMLUnitNG,参考了讨论区伦佬的方法。补全了MyPath类的JML规格,然后生成了一组测试样例进行测试。但是可能规格没有设计清楚,导致测试时出了一些问题。
第三次作业
由于新增的方法,大多都是查询方法。所以这次作业我主要采取随机测试,主要用来测试时间和正确性。由于我写了俩种算法(dijkstra和floyd),可以通过将进行对比。
JUnit是必要的,我将上次作业的测试拷贝过来,来保证上次作业的方法的正确性。
个人心得
在学习本单元之前,我对于关于形式化方法的理解不够深入。通过这个JML的学习,我认识到可以从架构设计层面就开始形式化。有了JML,每个类之间的设计、调用可以不依赖类的具体实现,根据规格就可以知道该如何使用该类、该方法。因此学习JML是非常有必要的,JML也可以用在今后设计之中。