OO第三次博客作业--JML之旅
1.梳理JML语言的理论基础、应用工具链情况
a.注释结构
JML以javadoc注释的方式表示规格,每行以@起头。注释方式有两种:行注释//@ annotation
和块注释/*@ annotation @*/
。注释一般放在被注释成分的临近上部。例如:
public abstract class IntHeap { //@ public model non_null int [] elements; /*@ public normal_behavior @ requires elements.length >= 1; @ assignable \nothing; @ ensures \result == (\max int j;0 <= j && j < elements.length;elements[j]); @*/ public abstract /*@ pure @*/ int largest(); };
b.方法规格
方法规格的核心内容包括三个方面:前置条件、后置条件和副作用约定。
-
前置条件 :
/*@requires*/
对方法输入参数的限制,不满足则不能保证正确性。 -
后置条件 :
/*@ensures*/
对方法执行结果的限制,执行后如果满足则执行正确。/*@ requires size < limit && !contains(elem);@ ensures \result == true;@ ensures contains(elem);@ ensures (\forall int e;e != elem;contains(e) <==> \old(contains(e)));@ ensures size == \old(size) + 1;@*/public boolean add(int elem) { /*...*/}
-
副作用约定 :
/*@assignable*/
指方法在执行过程中对输入对象或 this 对象进行了修改(对其成员变量进行了赋值,或者调用其修改方法)。方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。①不允许在副作用约束子句中指定规格声明的变量数据。
②private量通常调用者不可见。可以用
/*@ spec_public @*/
注释一个类的私有成员变量,表示在规格中可以直接使用,从而调用者可见。public class IntegerSet{ private /*@spec_public@*/ ArrayList
elements; private /*@spec_public@*/ Integer max; private /*@spec_public@*/ Integer min; /*@ ... @ assignable \nothing; // assignable--可赋值 @ assignable \everything; @ modifiable \nothing; // modifiable--可修改 @ modifiable \everthing; @ assignable elements; @ modifiable elements; @ assignable elements, max, min; @ modifiable elements, max, min; @*/}
c.原子表达式
-
\result : 非void型方法执行结果。
-
\old(expr) : 表示表达式expr在相应方法执行前的取值。 该表达式涉及到评估 expr 中的对象是否发生变化,但是针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。
-
\not_assigned(x,y,…) : 用来表示括号中的变量是否在方法执行过程中被赋值。 该表达式主要用于后置条件的约束表示上(限制一个方法的实现不能对列表中的变量进行赋值)。
-
\not_modifed(x,y,…) : 表达式限制括号中的变量在方法执行期间的取值未发生变化。
-
\nonnullelements(container) : 表示 container 对象中存储的对象不会有 null。
-
\type(type) : 返回类型type对应的类型(Class)。
e.g. type(boolean)为Boolean.TYPE
-
\typeof(expr) : 该表达式返回expr对应的准确类型。
e.g. \typeof(false)
d.量化表达式
-
\forall : 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
(\forall inti,j; 0 <= i && i < j && j < 10; a[i] < a[j]) // 如果表达式为真,则说明 a是升序数组
-
\exists : 存在量词修饰的表达式。
(\existsint i; 0 <= i && i < 10; a[i] < 0) // 针对 0 <= i < 10,存在 a[i]小于0
-
\sum : 返回给定范围内的表达式的和。
(\sum int i; 0 <= i && i < 5; i) // 计算 0 + 1 + 2 + 3 + 4
-
\product : 返回给定范围内的表达式的连乘结果。
(\product int i; 0 < i && i < 5; i) // 计算 1 * 2 * 3 * 4
-
\max : 返回给定范围内的表达式的最大值。
(\max int i; 0 <= i && i < 5; i)
-
\min : 返回给定范围内的表达式的最小值。
(\min int i; 0 <= i && i < 5; i)
-
\num_of : 返回指定变量中满足相应条件的取值个数。
(\num_of int x; 0
e.约束限制
-
不变式限制(invariant) 不变式(invariant)是要求在所有可见状态下都必须满足的特性。
可见状态(visible state):
-
对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
-
在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
-
在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
-
在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
-
在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
-
在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻
-
public class Path{ private /*@spec_public@*/ ArrayListseq_nodes; private /*@spec_public@*/ Integer start_node; private /*@spec_public@*/ Integer end_node; /*@ invariant seq_nodes != null && @ seq_nodes[0] == start_node && @ seq_nodes[seq_nodes.legnth-1] == end_node && @ seq_nodes.length >=2; @*/}
凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态(完整可见)。在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调在任意可见状态下都要满足不变式。
静态不变式(static invariant) 和 实例不变式(instance invariant)。其中静态不变式只针对类中的静态成员变量取值进行约束,而实例不变式则可以针对静态成员变量和非静态成员变量的取值进行约束。可以在不变式定义中明确使用instance invariant
或 static invariant
来表示不变式的类别。
-
状态变化约束(constraint) 用constraint来对前序可见状态和当前可见状态的关系进行约束。
public class ServiceCounter{ private /*@spec_public@*/ long counter; //@ invariant counter >= 0; //@ constraint counter == \old(counter)+1;}
static constraint指涉及类静态成员变量,而instance constraint可以涉及类的静态成员变量和非静态成员变量。
f.应用工具链情况
常用的工具有OpenJML 、 JUnit 、JMLUnitNG等。Junit是一个单元测试包,可以通过编写单元测试类和方法,来实现对类和方法实现正确性的快速检查和测试。
2.部署JMLUnit,针对Graph接口的实现自动生成测试用例,分析数据
这里参考了讨论区大佬的方法,跑了一下测试,但是感觉对于验证程序的正确性来说还是有些薄弱。
我在规格作业的设计中,使用了Junit进行单元测试,体验不错。
3.梳理作业架构设计
第一次作业
第一次作业的设计比较简单,在MyPathContainer和MyPath中我都用了两个Hashmap来管理数据方便查询,因为别的方法都是按照规格实现的,没有什么很精妙的地方。
第二次作业
第二次作业完全在第一次作业的基础上进行扩展,MyPath类直接搬运了第一次作业。
在初始进行架构设计的时候,我和同学讨论,觉得将无向图专门设置成一个类UndirectedGraph,然后对MyPathContainer进行相应修改,在MyGraph中私有化这两个类的成员变量进行操作。但是在实现之后,我认为这会导致三个类的耦合度过高,而且产生了很多重复的方法,很多代码没有必要,所以在重构的时候直接将UndirectedGraph类和MyPathContainer类进行了整理和归并形成MyGraph类。其中的具体方法仿照第一次作业稍作修改即可。
本次作业选用了Floyd算法进行图的更新和管理,也不是很复杂,难度较小。
第三次作业
第三次作业的难度有所提升,也是我思考时间最长的一次,因为写了很久的架构继承关系十分混乱,所以模仿同学进行了重构,这里要对在此次作业中给予我耐心帮助和指导的同学表示感谢,真的学到了很多,此次作业也让我意识到了自己的水平还有很大的提升空间,自己和大佬的差距真的很大。
架构的关键点设计
a.设计了自己的Graph类,内部用三维数组管理4个边权图,使用编号进行调用
调用时需要用包名.类名进行调用。这里有点坑,在本次作业的过程中,我本来没有将其放入包中,但是idea会报错,经检查发现官方接口中已经给了Graph类,只能用包名来区分。
b.Graph类内部使用染色法进行dfs,用于查找联通块数目,连通块数目即为颜色值
c.Graph类内部依旧用了Floyd算法进行图的权值更新,采用了讨论区王嘉仪同学的算法,没有拆点。
d.关于PathGragh类,我在同学的建议下新增了这个类进行优化,大大地提升了速度。
PathGragh的主要优点在于,保存每个Path的Graph,在后续图结构变更的时候,只需要更新Path对应的Graph的权值,不需要对之前已经存在的Path再一次应用floyd进行更新。
4.分析作业bug情况
三次作业总的来说,难度比前两单元小了很多,而且也可以通过Junit、通过对拍进行测试,调试测试都要比第二单元的多线程都要轻松很多,没有性能分的话只要保证复杂度和正确性就可以。这也是我OO课程里唯一一个强测全部拿到满分的单元。之前的作业除了一次自己犯傻少加一行判断条件被强测炸掉,其他的作业虽然没有被查出来bug但是总因为性能分太差被虐,所以这个单元也让我获得了一些小小的成就感(虚假的快乐)。
5.对规格撰写和理解上的心得体会
规格单元相对于前两个单元,真的是十分友善了,上手难度并不大,对于一些简单的方法和类,我也可以很快理解它们的规格。我认为撰写规格的初衷,在于保证程序设计的逻辑严密性和正确性,每次看到指导书中“可以理解为,只要符合规格,就能保证程序的正确性”,我都感受到了一股神秘力量。前两次作业就是直接按照规格进行设计的,第二次作业虽然稍微麻烦一点,但因为第一次作业的铺垫,所以还是很好完成的。
按照规格去写程序,对于我们程序的扩展性也很有帮助。在完成第三单元代码作业的时候,明显感觉前两个单元顺畅许多,不需要到”重构火葬场“去。即使我中间对于架构调整了很多,也都是在细节的地方进行修改,根据需求调整算法即可,没有花费太多的精力。
第三次作业个人感觉和规格的关系不大,好像数据结构和算法的成分要大一点,不过这是我的个人体验,可能有悖于课程组的初衷(因为确实在后期实现的时候,规格的帮助确实不大)。第三次作业的收获相对更多一些吧,我学到了很多设计和算法上精巧的地方,感谢同学的耐心帮助。