一.梳理JML的语言基础和应用工具链情况。
(1)JML语言理论基础。
第三单元的任务主要是基于JML规格实现某些类中的某些方法,因此,读懂JML就成了完成此次作业的第一步。现对本单元比较常用的JML表达式做一些梳理。
语句部分:
- \old(expr)表达式:用来表示expr再放映方法执行前的取值。值得注意的是,作为一般规则,在使用该方法时,应用括号把expr作为整体括起来。例如:
@ ensures (\exists int i; 0 <= i && i < \old(pList.length); \old(pList[i].equals(path)) &&
@ \result == \old(pidList[i]));
- \result表达式:表示一个非void类型的方法执行所获得的结果,即方法执行后的返回值。\result表达式的类型就是方法申明中定义的返回值类型。例如:
@ ensures (\exists int i; 0 <= i && i < \old(pList.length); \old(pList[i].equals(path)) &&
@ \result == \old(pidList[i]));
- \forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。例如:
@ ensures (\forall int i; 0 <= i && i < pidList.length; pidList[i] != pathId);
- \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应约束。例如:
@ ensures (\exists int i; 0 <= i && i < \old(pList.length); \old(pList[i].equals(path)) &&
@ \result == \old(pidList[i]));
方法规格:
- 前置条件:通过requires子句表示:requires P;。前置条件是调用者调用当前方法时当前环境必须满足的条件,但是在当前方法中不需要实现该判断。注意同一个方法的正常功能的前置条件和异常功能的前置条件一定不能重叠,否则会引法严重的设计错误。根据LSP原则,任何基类出现的地方,都能用子类去替换,因此如果B继承A,在相应方法的前置条件处,可以放宽requires的条件。
- 后置条件:通过ensures子句来表示:ensures P;。表明方法实现者确保方法执行返回结果一定满足谓词P的要求。规格中可以有多个ensures子句,方法实现者必须同时满足所以ensures子句的要求。根据LSP原则,如果B继承A,在相应方法的后置条件处,可以缩紧ensures的要求。
- 副作用范围限定:用assignable或modifiable表示,指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。在继承时,由于父类看不到子类中新增的属性,因此即使子类中因为对额外属性进行操作导致父类与子类中的assignable不一样,只要父类中的属性值变化相同,则也是正确的。
- also子句:两种使用场景:(1)父类中相应方法定义了对各,子类中邪恶该方法,需要补充规格,这是应在补充的规格之前使用also;(2)一个方法规格中涉及多个功能规格描述,正常功能规格或者异常功 能规格,需要使用also来分隔。
- signals子句:signals(***Exception e) b_expr。意思是当 b_expr 为 true 时,方法会抛出括号中给出 的相应异常e。注意, 如果一个方法在运行时会抛出异常,一定要在方法声明中明确指出(使用Java的 throws 表达式),且必须确保 signals子句中给出的异常类型一定等同于方法声明中给出的异常类型,或者是后者的子类型。
规格类型
- 不变式invariant:invariant P。不变式是要求在所有可见状态下都必须满足的特性,凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态。换句话说,在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调 在任意可见状态下都要满足不变式。不变式中可以直接引用pure形态的方法。
- 状态变化约束constraint:constraint P。constraint用来对前序可见状态和当前可见状态的 关系进行约束。invariant和constraint可以直接被子类继承获得。
(2)应用工具链情况
JML编译器,如OpenJML。
-OpenJML的一个基本功能就是对JML注释进行检查,包括经典的类型检查、变量可见性与可写性等检查。通过Check——Run Check即可进行类型检查。(对于注释编译的报错和警告信息,笔者每次都觉得很棘手,经常不知道该如何下手进行修改(′д` ))
-check只能检查注释语法的正确性,如果想对规格内容进行检查,需使用-esc参数,如果是静态检查,需要指定prover。
-使用-rac选项可以执行运行时检查。JML UnitNG结合OpenJML可以生成一个java类文件测试的框架,实现对代码的自动化测试。
JMLdoc,与javadoc类似,但其生成的Html格式文档中包含JML规范。
二. 部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果(跳过)
三. 部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例(简单方法即可,如果依然存在困难的话尽力而为即可,具体见更新通告帖), 并结合规格对生成的测试用例和数据进行简要分析
通过java -jar jmlunitng.jar使用适当的命令行选项运行JMLUnitNG()来生成测试类。
修改测试数据生成策略以添加自定义测试数据(有关生成的文件本身的信息,请参阅下文)。如果不更改测试数据生成策略,它们将使用默认数据。基本类型数据默认值与JMLUnit相同(例如,-1 / 0/1 int);
使用常规Java编译器编译生成的类,使用适当的JML运行时Jar和JMLUnitNG Jar CLASSPATH。如果使用的是OpenJML RAC,则只需要JMLUnitNG Jar CLASSPATH,因为JMLUnitNG Jar包含OpenJML。
运行测试。这可以通过编写一个 testng.xml文件来运行所有测试,通过从命令行单独运行测试类(每个测试类都有一个main方法),或者从命令行运行TestNG并将其指向测试类来完成。
针对Openjml写了一个测试程序Test.java,其内容是两个整型相乘,得到一个整型。代码及Openjml运行结果如下:
运行结果如下:
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor Test()
Passed: static main(null)
Passed: static main({})
Passed: static mult(-2147483648, -2147483648)
Passed: static mult(2147483648, -2147483648)
Passed: static mult(0, -2147483648)
……
===============================================
Command line suite
Total tests run: 13, Failures: 1, Skips: 0
根据测试结果可见,JMLUnitNG主要针对临界值进行测试。
四. 按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构
第一次作业的架构如下。
Path以及PathContainer采用的内部存储结构都是hashmap。键值数组是对应的index,值类型是数组对应的类型。因为Path在创建后不可改变,所以只在构造函数内有hashmap的插入。
第二次作业的架构如下。
第二次作业与第一次作业有很大不同。需要将多条path转换为无向图。因为有求distinctNode的数量的指令,而且指令数量非常多,因此每次都去求一遍是非常困难的,复杂度超标,而add path和remove path指令数量比较少,所以需要设置一个变量值,在每次add path和remove path时来修改这个变量。需要时直接返回这个变量即可。求最短路径时采用floyd算法。
第三次作业的架构如下。
第三次作业和第二次作业的架构差不多。但是hashmap的数量显著增多,在add path和remove path时需要修改的hashmap很多,造成自己思路不清晰,很容易出现bug。这次由于即使出发点和到达点一致,但path的编号不同,已经不能采用floyd算法,所以采用评论区大佬的拆点然后dijkstra算法。
五. 按照作业分析代码实现的bug和修复情况
第一次作业没有发现bug。
第二次作业没有发现bug。
第三次作业的bug在于Arraylist的remove方法,当Arraylist装的是Integer时,不能直接remove(变量名),因为Arraylist的remove是重载方法,既有remove(Object obj),又有remove(Integer index),所以需要使用remove(Integer.valueOf(value))来进行remove操作。
六. 阐述对规格撰写和理解上的心得体会
规格作业难度整体来说比电梯作业低,学习了如何书写规格和如何根据规格写代码,规格只是规定了一个方法需要达到什么效果,具体实现方法还是根据自己实际情况而定。
我感觉除了规格和代码之间的翻译工作之外,本单元还是更注重数据结构,因为卡了时间复杂度,如果没有良好的数据结构很容易tle,在设计过程中不光要注意架构,也要注意数据结构,还要特别注意Arraylist装整型时的remove这种细节问题。不注意细节一时爽,强测火葬场。