1.1 JML语言理论基础
JML是Java建模语言。使用 JML 来说明性地描述所希望的类和方法的行为,可以显著地改善整个开发过程。将建模表示法添加到 Java 代码中。JML的使用可以更加精确地描述代码所完成的任务、有效地发现和纠正错误、减少随着应用程序的进展而引入错误的机会、产生始终与应用程序代码保持同步的精确文档......
(1)格式说明
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式 为 //@annotation ,块注释的方式为 /* @ annotation @*/
(2)表达式
①原子表达式 :
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值
\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false
\not_modified(x,y,...)表达式:该表达式限制括号中的变量在方法执行期间的取值未发生变化
\nonnullelements( container )表达式:表示 container 对象中存储的对象不会有 null
\type(type)表达式:返回类型type对应的类型(Class)
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。此外,JML专门又定义 了如下四类操作符。
E1<:E2 :如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假
(3)方法规格
方法规格的核心内容包括 三个方面,前置条件、后置条件和副作用约定。
前置条件通过requires子句来表示: requires P;。其中requires是JML关键词,表达的意思是“要求调用者确保P为 真”。
副作用约定用关键词 assignable 或者 modifiable表示 :副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
类型规格指针对Java程序中定义的数据类型所设计的限制规则,常用的是不变式限制和约束限制 。无论哪一种,类型规格都是针对类型中定义的数据成员所定义的限制规则,一旦违反限制规则,就称 相应的状态有错。
不变式(invariant P ):invariant 为关键词,P为谓词。要求在所有可见状态下都必须满足的特性。其中,可见状态是指,修改成员变量的方法执行之外的状态。
状态变化约束(constraint):对对象的状态在变化进行约束的不变式。对前序可见状态和当前可见状态的关系进行约束。
1.2 JML应用工具链
可以使用开源的JML编译器来编译含有JML标记的代码,所生成的类文件会在运行时自动检查JML规范,若程序未实现规范中规定的事情,JML运行期断言检查编译器会抛出一个unchecked exception来说明程序违背了哪一条规范。目前,许多基于JML的验证、调试和测试工具已经非常成熟,例如运行时刻的断言检查器(RuntimeAssertionChecker)、JmlUnit、JMLAutoTest等等。JMLdoc工具与Javadoc工具类似,可在生成的HTML格式文档中包含JML规范,JMLUnit可以生成一个Java类文件测试的框架。SMT Solver工具可以以静态方式来检查代码实现对规格的满足情况。
二、SMT验证
pass
三、JML测试
3.1 openjml工具链
1、-check检查规格格式
public class demo { /*@ public normal_behaviour @ ensures \result == ((lhs - rhs)>0); */ public static int compare(int lhs, int rhs) { //wrong return (lhs>rhs); } public static void main(String[] args) { compare(114514,1919810); } }
对于compare方法检测jml规格,这个错误版本中出现,return方法返回int型,但是(lhs>rhs)比较得到boolean型,与方法返回类型不符,openjml检查不通过
public class demo { /*@ public normal_behaviour @ ensures \result == ((lhs - rhs)>0); */ public static boolean compare(int lhs, int rhs) { //right return (lhs>rhs); } public static void main(String[] args) { compare(114514,1919810); } }
修改后,-check检查通过
2、 -rac 运行时检查
以检查溢出为例
正确比较版本奉上:
public class demo { /*@ public normal_behaviour @ ensures \result == false; */ public static boolean compare(int lhs, int rhs) { //right return (lhs>rhs); } public static void main(String[] args) { compare(-2147483648,2147483647); } }
用直接用<比较,避免了溢出错误。
错误比较版本如下:
public class demo { /*@ public normal_behaviour @ ensures \result == false; */ public static boolean compare(int lhs, int rhs) { //wrong return ((lhs-rhs)>0); } public static void main(String[] args) { compare(-2147483648,2147483647); } }
作差比较法,算出溢出导致错误。
3.2jmluniting
没有太搞懂jmluniting如何使用,我利用讨论区中在idea中利用testMe插件检查正确性的方式检查了我第三次作业的MyPath类几个方法
package src; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import static org.testng.Assert.*; public class MyPathTest { private static int[] path1 = {1, 2, -2147483648, 2, 20}; private static int[] path2 = {1, 2, 2147483647, -1, 0}; MyPath pa1 = new MyPath(path1); MyPath pa2 = new MyPath(path2); MyPath pa3 = new MyPath(path1); @Test public void testCompareTo() { int answer1 = pa1.compareTo(pa2); int answer2 = pa2.compareTo(pa1); int answer3 = pa3.compareTo(pa1); assertTrue(answer1 < 0); assertTrue(answer2 > 0); assertEquals(answer3,0); } @Test public void testSize() { assertEquals(pa1.size(),5); } @Test public void testGetNode() { int node = pa1.getNode(2); assertEquals(node,-2147483648); } @Test public void testContainsNode() { assertTrue(pa2.containsNode(1)); assertFalse(pa3.containsNode(-1)); } @Test public void testGetDistinctNodeCount() { assertEquals(pa1.getDistinctNodeCount(),4); } @Test public void testEquals() { assertTrue(pa1.equals(pa3)); } @Test public void testIsValid() { assertTrue(pa1.isValid()); int[] path = {1}; MyPath pa = new MyPath(path); assertFalse(pa.isValid()); } }
主要针对性的测试了compare方法,检查比较是否依照字典序进行,满足jml规格要求,着重检查int型算出溢出问题,防止作差比较出现算出溢出导致判断错误。检查了containsdistinctnode方法,对于设了重复结点的path1,检查方法的实现是否避开了重复统计。
四、作业架构梳理
4.1 JML第一次作业
(1)架构说明
MyPath类中用ArrayList保存node序列,动态数组方便对数组的动态操作。Hashset存储不同结点,主要为了服务查找path中不同结点的getDistinctNodeCount方法。重写equals方法与hashCode方法,方便在MyContainer中把Path作为HashMap的key进行比较查重。
MyContainer类中设计两个HashMap p2id以及id2p分别记录path与pathid的对应关系,又设立HashMap存储node和node在container中的数量,方便getDistinctNodeCount方法的实现。curid是累计pathid的静态变量。
(2)架构分析
这次作业的MyContainer设计中由于no2num的存在导致addPath方法与removePath方法复杂度高,因为每一次添加路径都需要遍历路径中的结点,对container中的数组node->num的hashMap进行修改。
4.2 JML第二次作业
(1)架构说明
第二次作业我沿用了第一次的MyPath类。
作业新增需求最短路径与判断两个结点是否相连,在第一次作业的基础上Graph类中通过BFS判断两个node是否相连,同时将两个结点之间最短路径以及bfs路上的起始结点到路过结点的最短距离保存到cache nodes2shortest中。为了bfs操作,需要添加类属性node2nodes,记录某一个点到与这个点邻接的点集合,相当于图的邻接矩阵中的出边。为了在graph类的添加删除路径中维护node2nodes的正确性,需要建立edge2id这个HashMap,记录一对邻接点与两个点作为邻接关系出现的Path集合。为了这个edge2id的HashMap实现,我又设立Edge类,表示邻接点的无向邻接结构,即node1->node2与node2->node1等价。
Edge类是我为了结点之间邻接关系的表述构建的类,在这个类中,主要重写hashcode方法与equals方法。
(2)架构分析
node2nodes与edge2id的出现使我对MyGraph类中删除路径相关的方法变得愈加复杂。主要原因是删除路径的时候,我需要遍历路径中所有相邻结点,把改变edge2id中的id数组,同时有可能变更node2nodes中的结点出边情况。为了维护操作的安全性,我还需要防止空指针操作爆出异常,所以这个方法在我的一通操作下复杂度极高,稍显不美观。
4.3 JML第三次作业
(1)架构说明
这一次作业MyPath类和Edge类基本沿用第二次作业的架构,两个类的存在意义与上一次作业也相同,不再赘述。
RailWay类在上一次作业基础上新增的需求是计算连通块个数,计算两个点之间最少换乘次数,基于换乘计算两点之间最低票价以及最低不满意度。对于新增的需求,我第二次作业的架构无法支撑不同路径之间"换乘"的体现。最后我为了体现换乘选择“拆点”,单独建立subGraph类,将不同路径之间的交叉点区分开,并在交叉点之间连上权重为换乘代价的路径。用ed2len结构记录面向某种计算需求的子图的初始边权图。在subGraph中no2plt和no2plat记录node的拆分信息。由于最短路径的计算我是在subGraph中通过迪杰斯特拉实现的,因此我又设计了cache属性,记录暂存每一次dij的结果。
注意到subGraph类中引用了Mycompare类,这个类是我为了迪杰斯特拉中优先队列的构建自定义的比较类。
说会RailWay类,对于换乘次数,最低票价,最低不满意度计算,我分别建立trans,ticket,unhappy三个一级缓存,因为我的subgraph中出发点与到达点之间是有向关系,subgraph的缓存也是有向的,因此我在railway中建立无向的一级缓存。对于连通块,我把连通块的计算与connect合并,通过bfs先对railway中每个结点染色分类,node2group和blocks记录分类情况,这样在每一次更新地铁线路时都重新分块一次。最短路径的计算依旧采用bfs,数据结构也不改变。
(2)架构分析
五、代码实现bug与修复
在第一次作业中比较容易出错的点是compare中不是用大小比较关系比价两个int型而是作差比较,这种比较方式在int边缘时会发生溢出,导致结果错误。所幸自己并没有出现这样的问题。也没有写出其他bug
在第二次作业,提交截止前,我的代码中removePath方法中潜藏着空指针异常的隐患。我在本地测试时,构建出现重复点的Path,比如1,2,3,2,1,我原本的代码设计中(2,3)如果从edge2id中被删掉key,我对于下一次取到(3,2)是空指针没有进行处理,导致后面取到了一个null,在null中删除id操作导致空指针异常,万幸我在提交之前发现了问题,没有酿成大祸。
六、JML规格心得体会
JML规格书写有点像离散1...
总的来讲,JML规格书写要求一个方法不可以设计过分复杂,要把具体需求拆分到不同方法中实现,让每一个类方法清爽简洁,满足单一责任原则(SRP)设计规则。比较遗憾的是,这三次作业中,我的一些方法,比如remove Path方法以及求最短路径相关算法的实现都没有自己重新设计JML规格,最后写出来依旧是复杂度高,在一个方法中完成了过多的需求,不太美观。