OO第三单元作业总结
JML是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言,基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,我们可以基于规格自动构造测试用例。利用JML开展规格化设计,这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
JML语言的理论基础和应用工具链
JML语言的理论基础
方法规格是JML的重要内容,方法规格的核心内容包括三个方面,前置条件、后置条件和副作用约定。其中前置条件是对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性;后置条件是对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。副作用指方法在执行过程中对输入对象或 this 对象进行了修改(对其成员变量进行了赋值,或者调用其修改方法)。课程区分两类方法:全部过程和局部过程。前者对应着前置条件恒为真,即可以适应于任意调用场景;后者则提供了非恒真的前置条件,要求调用者必须确保调用时满足相应的前置条件。从设计角度,软件需要适应用户的所有可能输入,因此也需要对不符合前置条件的输入情况进处理,往往对应着异常处理。从规格的角度,JML区分这两种场景,分别对应正常行为规格和异常行为规格。
方法规格
-
前置条件
前置条件通过requires子句来表示: requires P;
。其中requires是JML关键词,表达的意思是“要求调用者确保P为 真”。就是调用这个方法前我们需要满足的前置条件。
-
后置条件
后置条件通过ensures子句来表示: ensures P;
。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。就是如果输入满足前置条件那调用这个方法后输出一定会满足的条件。
-
副作用范围限定
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词assignable或modifiable。就是调用这个方法的过程中会修改哪些量。
类型规格
-
不变式
不变式是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P
,其中 invariant
为关键词, P 为谓词。
-
状态变化约束
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
JML应用工具链
openJML:静态检查JML语法的正确性。
JMLUnitNG:根据JML自动生成测试文件。
JMLUnitNG
代码:
// demo/Demo.java
package demo;
public class Demo {
/*@ public normal_behaviour
@ ensures
esult == a - b;
*/
public static int compareTo(int a, int b) {
return a - b;
}
public static void main(String[] args) {
compareTo(12345,14325);
}
}
命令:
jmlunitng Demo.java
javac -cp jmlunitng.jar *.java
openjml -rac Demo.java
java -cp jmlunitng.jar Demo_JML_Test
结果:
[TestNG] Running:
Command line suite
Passed: racEnabled()
Passed: constructor Demo()
Passed: static compareTo(-2147483648, -2147483648)
Failed: static compareTo(0, -2147483648)
Failed: static compareTo(2147483647, -2147483648)
Passed: static compareTo(-2147483648, 0)
Passed: static compareTo(0, 0)
Passed: static compareTo(2147483647, 0)
Failed: static compareTo(-2147483648, 2147483647)
Passed: static compareTo(0, 2147483647)
Passed: static compareTo(2147483647, 2147483647)
Passed: static main(null)
Passed: static main({})
===============================================
Command line suite
Total tests run: 13, Failures: 3, Skips: 0
===============================================
架构设计
第一次作业在MyPath里用一个ArrayList来存结点序列,用一个TreeSet来存不同的节点。在MyPathContainer中我是用TreeMap实现Path到id的映射,用Path数组实现id到Path的映射,用TreeMap实现不同结点到其出现的次数的映射。利用这样的存储结构能在添加删除时就把不同的结点记录下来并算出不同结点个数,节省时间。
第二次作业MyPath和上次一样,MyGraph是直接在第一次的MyPathContainer上改的(复制粘贴真方便)。相比于第一次多了一个由Integer到Integer的HashMap和一个存TreeSet的ArrayList的邻接表,以及存最短距离的二维数组。具体算法是先构造邻接表,然后用bfs读入邻接表进而计算出最短距离矩阵,最后根据距离矩阵算是否可达。
第三次作业MyPath和上次一样,MyRailwaySystem依旧是复制粘贴上一次的MyGraph然后在上面改。这次由于要求比较多,所以新开了算加权图最短路径的类,以及边信息的类。本次采用的是拆点法,具体实现方法感觉不容易说清楚,见下图
简单地说就是把每个点都视为换乘结点,每条路单独存,这样生成的路径矩阵就能变成120 * 120(实际上为了使用Dijstra算法用120 * 4000更加方便)很大程度上减少了查找的时间。
bug及修复情况
这一单元的作业由于给了规格,只要按规格写就不怎么会出现bug,所以bug并不多,在第二次作业中因为我一开始没考虑自环的情况,最短距离矩阵对角线上都是0,后来发现了这个bug就把这个情况考虑了进去,但是之前写的bfs判断退出的一个条件是如果找到了n-1个点的最短距离就退出,由于加了自环所以这个退出判断应该改成n,当时没有发现就提交了,然后在强测和互测中被测出了bug。
心得体会
感觉这个单元比前两个单元都要简单,其中很重要的原因是因为给了规格,这也看得出来一个正确的规格对于写代码的重要性,如果规格写的很清楚,代码只需要照着规格实现即可,真的很方便前提是我能写出这么严谨的规格。这也改变了我一贯的写代码风格,学了这一单元后我懂得了要先考虑架构设计再来考虑算法,这样在写代码前就能进行测试,在写代码前就能规避很多不必要的bug。