OO第三单元作业分析——JML与自动化测试初试

一、JML(Java Model Language)的理论基础与工具链

JML是用于对Java程序进行规格化描述的一种表示语言,广泛应用于契约式设计(Design by contract),形式化证明(Formal validation)等等理念当中。它是一种行为接口规格语言(Behavior Interface Specification Language),基于Larch方法构建。直观上讲,如果把Java程序当作可使用的产品或者应用,而程序员则是设计师的话,那么JML就相当于一份用户需求书。它详细描述了程序所需要实现的功能以及造成的影响,而不介意其具体实现方法与数据结构。从效果的角度,JML同时也是对于程序的一种抽象,同时忽略了众多可能的实现的具体差异,抓住了其核心精髓而去其表面,进而提高代码的可维护性与复用性。因此,JML往往简洁而内涵丰富,读起来感觉晦涩而理解之后却又往往令人拍案叫绝,是一个强大而实用的工具。

以下,从功能及结构的角度对于JML的基础语法进行分类梳理

(参考资料:http://www.eecs.ucf.edu/~leavens/JML//index.shtml & https://en.wikipedia.org/wiki/Java_Modeling_Language

1.JML书写结构

与注释的使用大同小异,注意块注释每行都要以@开头

 //@annotation :行注释

 /*@ annotation @*/ :块注释

2.JML表达式2.1.原子表达式

 \result :表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。

 \old(expr) :表示一个表达式expr在相应方法执行前的取值。当expr有被修改时才会使用此表达式,表示expr被修改之前的值。这里有一点需要注意,对于一个引用对象,只能判断引用本身是否发生变化,即只能描述引用对象的地址是否发生变化,而不能描述对象的成员变量等是否发生变化。

 \not_assigned(x,y,…) :表示括号中的变量是否在方法执行过程中被赋值。

 \not_modified(x,y,…)  :表示限制括号中的变量在方法执行期间的取值未发生变化。

 \nonnullelements(container) :表示container对象中存储的对象不会有null。

2.2.量化表达式

 \forall :全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

 \exists :存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

 \sum :返回给定范围内的表达式的和。

 \product :返回给定范围内的表达式的连乘结果。

 \max :返回给定范围内的表达式的最大值。

 \num_of :返回指定变量中满足相应条件的取值个数。


为了更加有效地阐释,我们给出两个例子:

(\forall int i; 0 <= i && i < n; a[i] == 1) // 表示常数列
(\product int i; 1 <= i && i <= 3; i)  // 表示3! = 6
2.3集合表达式

 new ST {T x|R(x)&&P(x)} :其中的R(x)对应集合中x的范围,P(x)对应x取值的约束。

2.4操作符

仅介绍除了Java语言的算数操作符,逻辑运算符(这些也是JML允许使用的)之外的符号。

 E1<:E2 :子类型关系:当且仅当E1属于E2的子类时候为真。

 expr1 <==> expr2 :等价关系,类似还有<=!=>。请注意 <==> 比 == 的优先级要低,<=!=> 比 != 的优先级低

 expr1 ==> expr2 :当前仅当expr1 = true且expr2 = false时为false

 \nothing :空集

 \everything :全集

3.方法规格

方法规格是针对方法的执行过程而言的。其的核心是需要满足前置条件,后置条件和副作用约定三个要求(以下P为谓词)。

前置条件(pre-condition): requires P; 调用者在方法执行前必须满足的要求(比如对于输入要求,全局变量状态要求等等)。

后置条件(post-condition): ensures P; 方法执行之后会满足的条件。

副作用范围限定(side-effect): assignable element;  modifiable element; 表示element变量在执行后会产生副作用(有变化可能)。其中assignable表示element可赋值,modifiable则表示element可修改。特别地,assignable \nothing 表示无副作用。

其他:

正常行为规格(normal_behavior):满足前置条件的情形下的规格。

异常行为规格(expcetional_behavior):描述对于未满足前置条件的情形的执行结果,通常会抛出异常,使用signals字句表达。

 signals (Exception e) expr :当expr为true时,方法会抛出括号中给出的相应异常。

4.类型规格

类型规格则是从类或者对象的角度,描述对于一些变量的限制约束。

 invariant P  :要求在所有可见状态下都要满足P。可见状态是指,对象不会在被观察的时候发生变化,从而导致观察到状态与实际状态不符的情况。譬如构造方法的结束时刻就是可见状态,而回收方法(finalize)执行的时候就不是。

 constraint P :invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。

最后谈谈JML的工具链。我个人倾向于使用openJML,它可以通过自带的编译器对于JML规格进行检测。美中不足是,它似乎有时候有些过于严格,以至于想通过测试便要像写代码一样严格地来写JML,结果即便是课程组下发的JML也是整片的报错,更不要提自己写的了。

 

二、JMLUnitNG的部署与基础应用

关于JMLUnitNG的部署,必须要说的是如果使用IDEA,则强烈推荐使用命令行的方式进行安装。这个时候一定要注意Linux系统和Windows系统之间的差异。首先需要在官网下载jar包:http://insttech.secretninjaformalmethods.org/software/jmlunitng/assets/jmlunitng.jar

同时为了方便检查规格语法,我们最好也用一下openJML。

我们写好的测试如下:

 1 public class JMLTest
 2 {
 3     /* @ public normal_behaviour
 4        @ requires (homework >= 0 && homework <= 100 && midterm >= 0 && midterm <= 100);
 5        @ ensures \result = homework * 0.6 + midterm * 0.4;
 6        @ also
 7        @ public exceptional_behaviour
 8        @ requires (homework < 0 || homework > 100 || midterm < 0 || midterm > 100);
 9        @ signal_only Exception;
10      */
11     public static double gradeOfMyOO(double homework, double midterm)
12             throws Exception
13     {
14         if (homework < 0 || midterm < 0 || homework > 100 || midterm > 100)
15         {
16             throw new Exception();
17         }
18         return homework * 0.6 + midterm * 0.4;
19     }
20     
21     public static void main(String[] args) {
22         try
23         {
24             double grade1 = gradeOfMyOO(67,88);
25             double grade2 = gradeOfMyOO(92,97);
26             double grade3 = gradeOfMyOO(88,110);
27             double grade4 = gradeOfMyOO(-5,90);
28         }
29         catch (Exception e)
30         {
31             System.out.println("Invalid score!");
32         }
33         
34     }
35 }
testJML

第一步,运行openJML:

 java -jar openjml.jar -check test/testJML.java 

检查无误之后,就可以上NG了!(这里真的想吐槽一句,为啥openJML连\forall都不放过??)

 java -jar jmlunitng.jar test/Test.java 

运行结果如下所示:

 

三、单元作业架构设计分析

 

第一次作业的架构非常简单,就是实现MyPath和MyPathContainer两个类。其中MyPathContainer是Mypath为数据的一个容器。

 

 

第二次作业中,MyGraph仍然是MyPath的容器。由于此次结构比较复杂,因此我们增加了一个新的数据结构:TwoPoint(后面会具体介绍)。Path类并没有什么变化。

 

 

第二次作业中,MyGraph的方法已经非常繁杂了,如果第三次作业MyRailwaySystem还要从头开始写的话,代码量会明显超载,也不利于整体的架构平衡。因此我们选择让MyRailwaySystem继承自MyGraph,这样许多方法(不需要重写的)就不必再重复出现了,也有助于集中于新功能上。值得一提的是,从安全发布角度来看,子类是不应该能够直接改变父类的数据结构的,而应该提供相应的方法来完成这种操作。

四、单元作业代码实现与bug分析

首先先说一下策略问题。

事实上,我们有两种不同的思路出发点:一是懒惰策略(lazy strategy)。由于维护查找相关的数据结构不是轻松的事,因此我们选择不是每次增删路径的时候更新查找,而是在执行查找的时候再去说。这很像是操作系统课程里面的共享进程写时复制(Copy on write)机制。二是选择维护一个数据结构,增删路径的时候就同时更新,这样查找就会非常便捷高效。

考虑到三次任务的特点都属于多查找任务,因此第二种思路更加划算。

下面再谈谈每次作业的代码实现细节与问题。

第一次作业只需要严格按照规格进行即可。一定要说的话,就是对于数据结构的选取可以很大地影响运行效率。HashMap在查找,增删等操作上都比静态数组高效很多。除此之外,还有一个重要思路是“用空间换时间”采取冗余存储:

private HashMap<Path,Integer> pmap;
private HashMap<Integer,Path> imap;
private static HashMap<Integer,Integer> nodemap;
data structure of homework9

 其中pmap以path为key,根据path查找id的效率就是o(1);imap以id为key,根据id查找path的效率也是o(1)。但是如果只有一个的话,那么必然有一个是o(n)。第一次作业没有出现或找到别人的bug。

第二次作业和第一次作业相比,比较棘手的就是最短路径的查询。我们注意到,由于我们的思路是维护一个最短路径的表(记录任意两点的最短路径长度),因此每当路径发生变化的时候,我们都要更新一下,也就是考虑图中任意两点(如果连通)的最短路径。因此,选择Floyd算法是比较合适的。如果选取Dijkstra,一定要注意所谓的o(n^2)是针对一个点到其他点的路径而言,所以要想得到所有点的最短路径仍然是o(n^3),并没有更好,况且Floyd算法可以通过“剪枝”做进一步优化。第二次作业也没有出现或找到bug。

这里讲一下我新建的一个数据结构TwoPoint,简单来说就是一个无序数对。虽然一再强调,我们不难简单地将“面向对象”中的对象理解成数据结构,但是有时候如果能够巧用一下一些简单的struct还是能非常有效率的。这里使用TwoPoint类解决了我们的HashMap相同的key值只能存一个的问题(往下,我们几乎所有数据结构都是HashMap,且都是以TwoPoint作为key的)。

 1 public class TwoPoint
 2 {
 3     private int[] node = new int[2];
 4     
 5     TwoPoint(int a,int b) // 要求node[0] <= node[1]
 6     {
 7         if (a <= b)
 8         {
 9             node[0] = a;
10             node[1] = b;
11         }
12         else
13         {
14             node[0] = b;
15             node[1] = a;
16         }
17     }
18     
19     public int getnode(int index)
20     {
21         return node[index];
22     }
23     
24     @Override
25     public boolean equals(Object obj)
26     {
27         return (obj instanceof TwoPoint &&
28                 ((TwoPoint)obj).getnode(0) == this.node[0] &&
29                 ((TwoPoint)obj).getnode(1) == this.node[1]);
30     }
31     
32     @Override
33     public int hashCode()
34     {
35         return Arrays.hashCode(node);
36     }
37 }
TwoPoint class

注意要重写hashcode,这样在HashMap种=中就比较方便了,因为HashMap是根据hashcode和equals一起裁定是否相等的。

 第三次作业相比之下就要困难一些了。事实上,不论是最低票价,还是最少换乘,亦或最低不满意度,实际上都是一种变相的加权最短路径,只不过这里的权值是广义的概念(比如,不满足value(AB) = value(AC) + value(BC))。这里我借鉴了讨论区wjy同学的思路(https://course.buaaoo.top/assignment/75/discussion/210)。值得一提的是,最少换乘并没有必要像原文一样,取一个很大的值,只要路径内任意两点权值为1就可以(最终结果 - 1)。第三次作业仍然是通过HashMap数据结构,如下:

1 private HashMap<TwoPoint,Integer> distmap; // offset = 0
2 private HashMap<TwoPoint,Integer> pricemap; // offset = 2
3 private HashMap<TwoPoint,Integer> transfermap; // offset = 1
4 private HashMap<TwoPoint,Integer> unpleasemap; // offset = 32
5 private ArrayList<HashSet<Integer>> blocklist; // offset = 0
data structure of homework11

删除操作之后,最稳妥的思路自然是全部推倒重建,但这样显然非常不合算。我的做法是,在每条路径加入的时候,就保存好路径内部的一个图,这样当删除之后,我们只需要遍历所有路径,把所有路径的图一起整合一下即可。

五、总结与感想——严谨与效率间的纠葛

这一个单元的主要教学内容是JML,因此虽然作业似乎与JML的关系并不是非常密切,但是仍然想谈一谈关于JML的一些认识与启发。

首先不得不承认,JML是一个非常强大的工具,它能够严格地描述需求.......(都在第一段夸了)。但是从另一个角度来看,我觉得JML究其目的仍然应该被定为成伪代码的一种——伪代码的目的,便是以梳理思路,建立逻辑为主,而暂时忽视具体实现。这有点像我们在高速运转的课堂上匆匆记下的笔记,只要最终能看懂就成,至于好不好看就另说了,毕竟是以”备忘”为主。而JML虽然不能像我们的课堂笔记一样随意,但是如果能够保证所有相关人员都能懂,就可以了。所以这里还是想表示一下对于严格地“编译”JML语言的不解。

转载于:https://www.cnblogs.com/ZHONG-YU-1999/p/10902302.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值