OO总结之JML社交模拟
JML基础梳理及工具链
JMLUnitNG自动化测试
作业架构梳理
Bug综合分析
规格相关心得与体会
一. JML基础梳理及工具链
1.1原子表达式
-
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
-
\old(expr)表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
-
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
-
\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法 执行期间的取值未发生变化。
-
\nonnullelements( container )表达式:表示 container 对象中存储的对象不会有 null。
-
\type(type)表达式:返回类型type对应的类型(Class)。
-
\typeof(expr)表达式:该表达式返回expr对应的准确类型。
1.2量化表达式
-
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
-
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
-
\sum表达式:返回给定范围内的表达式的和。
-
\product表达式:返回给定范围内的表达式的连乘结果。
-
\max表达式:返回给定范围内的表达式的最大值。
-
\min表达式:返回给定范围内的表达式的最小值。
-
\num_of表达式:返回指定变量中满足相应条件的取值个数。
1.3集合表达式
-
集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
-
集合构造表达式的一般形式 为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
1.4操作符
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。此外,JML
专门又定义了如下四类操作符。
-
子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真。
-
等价关系操作符: b_expr1 <==> b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是 b_expr1 == b_expr2 或者 b_expr1!=b_expr2 。
-
推理操作符: b_expr1 == > b_expr2 或者 b_expr2 < == b_expr1 。对于表达式b_expr1 ==> b_expr2 而言,当b_expr1 == false,或者 b_expr1 == true 且 b_expr2 == true 时,整个表达式的值为true。
-
变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。
1.5方法规格
-
前置条件:前置条件通过requires子句来表示:requires P;。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
-
后置条件:后置条件通过ensures子句来表示:ensures P;。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。
-
副作用范围限定:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
1.6类型规格
类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计 的约束规则。-
不变式invariant :不变式(invariant是要求在所有可见状态下都必须满足的特性,语法上定义invariant P ,其中invariant为关键词, P为谓词.
-
状态变化约束constraint:状态变化约束(constraint)是对前序可见状态和当前可见状态的关系进行约束。
工具链:
-
OpenJML:可以检查JML文档规格语法,其中SMT Solver可以验证代码是否符合规格 。
-
JMLUnit / JMLUnitNG:用于进行单元测试,可重复运行的自动化测试。
二. JMLUnitNG自动化测试
经过各种调试以及对系统jdk版本的修改,最终在对规格进行魔改之后终于成功跑起来自动化测试。
调试过程略去,对第三次作业的IGroup
模块,即Group
接口的自定义实现类进行测试,结果如下:
从中可以看出,自动测试的生成的样例都是使用边界数据,只能检测一些最基础的边界情况,有点鸡肋。
不过从中也可以看出我对add和del方法的判断存在不当,没有判断null的情况。
三. 作业架构梳理
这三次作业的主要架构以及规格都已经给出了,我们只要负责实现即可。
第一次:
本次作业规定了最基本的社交功能,包括个人和网络类,其实现严格按照规格进行,且没有过多性能要求。
本次作业我的容器实现大多采用ArrayList
, 因为只需要实现简单遍历即可。在作业中本次的难点是isCircle函数,我采用的是bfs搜索,并且在person
类中加入getLinkedSet
方法来获取
与当前人有关系的人的集合。
第二次:
本次作业增加了群组类,并且实现了群组类基本功能,并对性能提出了较高的要求。
本次作业在设计之时依旧遵循规格书写,容器依旧采用ArrayList
,不过这也导致了一些严重的性能问题。
第三次:
本次作业加入图论算法,重点是对于算法的考察(确信)。
本次作业的时候我并没有了解并查集,所以我依旧使用bfs实现isCircle
,并且将isCircle
的重点部分抽取出来单独形成一个函数isCircleS
,以便在普通isCircle
和qsl
的不同调用场景下通用。 具体实现如下:
public boolean isCircleS(int id1, int id2,
Person exception,
boolean shc) {
if (id1 == id2) {
return true;
}
boolean shx = shc;
HashMap st = new HashMap();
st.put(getPerson(id1), getPerson(id1));
Iterator it = st.keySet().iterator();
while (it.hasNext()) {
IPerson cur = (IPerson) it.next();
ArrayList temp =
(cur).getLinkSet();
int j;
for (j = 0; j < temp.size(); j++) {
Person pj = (Person) temp.get(j);
if (pj.equals(getPerson(id2))) {
if (shx == false) {
shx = true;
continue;
}
exc = new ArrayList();
while (!st.get(cur).equals(getPerson(id1))) {
exc.add(cur);
cur = (IPerson) st.get(cur);
}
return true;
}
if (!st.containsKey(pj) && (exception == null || !exception.equals(pj))) {
st.put(pj, cur);
}
}
}
return false;
}
这个函数本身就是bfs的实现,并且可以做到在寻路之时排除一个特定的点,以及规定是否允许一步到位(即起点和终点是否可以直接相连),也可以在寻路完成后将本次寻得的路径保存在全局变量exc
中,每调用一次这个函数都会更新exc
。
在isCircle调用bfs是,把exception
设置为null,并且将shc设置为真,即允许一步到位。
在qsl调用第一次时,将排除设置为空,允许一步到位。然后根据更新的exc路径,遍历删除每一个点,分别测试起点终点是否依旧联通,并且不允许bfs一步到位,若有不能到达的情况
,则qsl返回false。
四. Bug综合分析
第一次作业:
本次作业的一个大bug就是忘记了在isCircle的bfs中忘记将bfs的搜索集合设置成不可重复集了,导致搜索可能进入死循环。
解决的方法就是注意剔除重复点即可。
第二次作业:
本次作业在性能上出现问题,主要集中在qmr上。
解决方法:在group中添加缓存机制,进组即计算。但是并没有解决qmr的问题,后经大佬点拨,发现可能存在的问题是在network中people使用的是ArrayList,导致contains方法多进行一次遍历
所以只要把ArrayList用HashMap替换,即可大幅提高效率。
第三次作业:
本次作业由于使用的算法比较稚嫩,包括迪杰斯特拉算法也是没有对优化的,也都导致了相应的超时。
解决方法:学习图论算法。
五. 规格相关体会及心得
本单元主要是规格的解读(?)捎带图论算法,虽然我现在依旧没有信心写出百分百完备的规格,但是基本能够进行解读
但实际上,规格仅仅就是另一种阐述需求的方式,具体的实现还是要靠自身的独特架构和能力,然而很明显在本单元前两次作业的时候我忽略了这点,导致非常窘迫。
很多同学在学习的过程中都是对JML(还有其配套的工具)表示质疑,因为在本次作业迭代的过程中JML并没有体现出什么显著的作用,就像是把指导书写在注释中一样。首先那JML本身来说,就我对其的理解,JML就是对你的代码进行形式化的推导,在操作系统安全相关知识的学习中我们了解到,可以进行形式化推导的系统的安全等级是要高于不能进行形式化验证的的,不过究其本质,在代码中加几行注释就会让它变得更安全吗?显然不是,但是为什么人们推崇形式化验证呢?就操作系统而言,形式化验证可以预知操作系统的所有行为,从而消除可能的旁路存在的可能性。说白了,就是让使用者更放心。JML作为一种注释性语言,更多的是为调用者服务,使其不必关心具体的实现方法,(前提是实现是正确的)。所以如果我们观察ide里JAVA自带类的文件,都可以看到一大串长长的JML注释,只需要阅读注释就可以了解如何使用。
然而本单元,我们是以实现者的身份去看JML,这反而没什么用了,因为JML是对我们要做的东西的一个抽象,就是用标准化语言告诉我们该干什么,那充其量也就是指导书。因为JML作为一种形式化契约,应该是由开发者提供,为调用者服务。
所以说,虽然经过了一个单元的学习,我对JML已经有了一定程度的理解,但是我还是有一个疑问,作为开发者,如果要是想让我们完全深入理解运用JML,难道不是应该让我们自己去编写JML(暴毙警告)去锻炼对编程的抽象化能力吗?至于算法,早晚都要面对,早点暴毙还可以充当一个警钟(误)。
总体上规格对于面向对象编程来说还是很有必要的,因为当你成为调用者,或者消费者时,它可以向你相对规范的解释你面前的这一大坨东西都能干什么,并且一个完备的JML规格可以进行形式化推导,对系统进行验证,还是很重要的。