OO第三单元总结—JML契约式编程
JML语言
综述
JML(Java Modeling Language)是用于对JAVA程序进行规格化设计的一种语言,很好地规定了行为规范。JML不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。非常地利于开发。
一般而言,JML有两种主要的用法:
- 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻
辑严格的规格。 - 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面
具有特别重要的意义。
语法
注: 很多jml规格看名称就很容易猜出意思
注释结构:
JML以javadoc注释的方式表示,每行开头以@开头。同时还分为行注释和块注释。
原子表达式
表达式 | 描述 |
---|---|
\result | 非void 方法的返回值 |
\old(expr) | 表示expr 在方法执行前的值 |
\not_assigned(x,y,...) | 表示括号中的变量x,y,...在方法执行过程中没有被赋值,一般用于后置条件 |
\not_modified(x,y,...) | 类似于\not_assigned(x,y,...)方法 |
\nonnullelements(container) | 表示container 中没有null |
\type(type) | 返回值的类型 |
量化表达式
表达式 | 描述 |
---|---|
\forall | 全称量词,表示范围内所有的元素均满足条件 |
\exists | 存在量词,表示在范围内存在元素均满足条件 |
\sum | 返回给定范围内的表达式的和 |
\product | 返回给定范围内的表达式的连乘结果 |
\max | 返回给定范围内的表达式的最大值 |
\min | 返回给定范围内的表达式的最小值 |
注
上面的许多表达式需要遍历变量,一般变量用i,j,k表示
如
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; 0 <= j && j < people.length &&
@ people[i].isLinked(people[j]); people[i].queryValue(people[j])));
@*/
public /*@pure@*/ int getValueSum();
操作符:
操作符 | 描述 |
---|---|
<: | 子类型关系操作符。eg: E1<:E2 如果E1是E2子类型则为真。 |
<==> | 等价关系,但比 == 优先级低 |
<=!=> | 与<==>含义相反。但比 != 优先级低 |
==> | 推理表达符,类比于数理逻辑的蕴含 |
\noting | 指示一个空集 |
\everything | 指示一个全集 |
方法规格
前置条件
主要通过requires
子句。使用requires P
语句,规定P对应的应当为真
后置条件
主要通过ensures
子句。使用ensures P
语句,规定P对应的应当为真。
副作用范围限定
一般使用关键词assignable
和 modified
,表示其后的可修改。
其他
- 纯粹查询方法
(/*@ pure @ */)
public normal_behavior
为正常功能public exceptional_behavior
为异常行为signals (***Exception e) b_expr;
表示b_expr成立时抛出异常Exception- 注意,
normal_behavior
和exceptional_behavior
不能有任何交集。
工具链
OpenJML
首先需要注意的是OpenJML在JDK版本过高时是无法运行的,需要将JDK版本调至8才能运行,同时编译路径最好不要出现中文,如果出现,加入语句-Dfile.encoding=utf-8
。
规格检查
使用 -check
检查Group参数(缺省)指定类型。但不能结合代码检查。
结果有警告:
不知道怎么改。。。
再结合SMT Solver 检查
#!/bin/bash
java -Dfile.encoding=utf-8 -jar E:/OpenJML/openjml.jar -exec E:/OpenJML/Solvers-windows/z3-4.7.1.exe -esc -dir
结果
不知道为什么。。
JMLUnitNG
JMLUnitNG编译方法大致和OpenJML方法类似,但要简单一些。主要需要运行其jar包。
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Passed: <>.addRelation(-2147483648)
Passed: <>.addRelation(-2147483648)
Passed: <>.addRelation(-2147483648)
Passed: <>.addRelation(2147483647)
Passed: <>.addRelation(2147483647)
Passed: <>.addRelation(2147483647)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Passed: <>.equals(null)
===============================================
Command line suite
Total tests run: 17, Failures: 7, Skips: 0
===============================================
Process finished with exit code 0
可以看到,其实这个构造的主要是边界数据,覆盖性并不是很强。
JUnit测试
以前都是用python,本单元尝试用了JUnit测试.
作业总结
本单元作业主要体现了JML契约化设计,即将JML规格下发,我们通过阅读规格,实现相应的代码。
第一次作业
设计分析
UML类图如下:
总的来说,本次作业架构较简单,只有三个类,而三个类的关系也比较明确.
Main
类为主类,没有太多需要注意的地方。
Myperson
实现Person
接口,除了像age
,character
等几个比较平常的字段外,还有熟人列表,这个也变成其复杂度主要来源,在这次我用两个ArrayList
分别实现acquaintance
和value
,因为数据很小,因此不太容易被卡。
MyNetwork
在本单元是核心类,在本次作业,其管理MyPerson
,其后的单元,它还管理MyGroup
,在本次作业时,我采取ArrayList
来管理Person
,通过遍历即可得到相关信息。
bug分析与修复
本次没进互测,原因是关键方法isCircle
方法规格看错了。这也提醒我认真读规格的重要性,看错规格即使对着方法进行再多的测试也是南辕北辙!
第二次作业
设计分析
UML类图如下:
总的来说,本次作业架构较之前复杂了许多,加入了Group
类。
Main
类为主类,没有太多改变。
Myperson
实现Person
接口,也没太多变化,但为了减小复杂度,将ArrayList
容器改为Hashmap
。
MyGroup
中实现将Person
归类,因此使用Hashmap
作为其容器,其中许多方法都需要遍历其中所有类,尤其是Relationsum
甚至需要两层循环,因此直接暴力的方法很难正确,于是我想动态维护,即每次Person
加入Group
时,更新group
的relationsum值;如果是添加关系,即遍历所有group,看起是否含Person如果有,就更新值,本来想着group的数量不超过10,有用的hashmap存储的,复杂度不会太高。
MyNetwork
在本单元是核心类,在本次作业,不仅管理了MyPerson
,还管理MyGroup
。同时为了减小复杂度,将所有ArrayList
改为Hashmap
。
bug分析与修复
在吸取了上次作业的教训后,本次作业及其认真地阅读了代码,也经过了大量的测试,保证了不会出现WA。
不过遗憾的是,代码复杂度太高,强测的许多点都CTLE了。
错误就是在我上面提到的动态维护,ar操作太过频繁,因此每次查询就会使得复杂度变得很高,这是我当时没有考虑到的,而在我互测屋里,也有同学是这样做的,估计也有很多点CTLE了吧。
因此,我取消了动态维护,直接改用二重循环,不过判断时使用的Hashmap
,意外的是,这样直接修复了全部的bug。分析原因,我算法方面还需要加强。
第三次作业
设计分析
UML类图如下:
总的来说,本次作业架构较之前复杂了许多,加入了Group
类。
Main
类为主类,没有太多改变。
Myperson
实现Person
接口,也没太多变化。
MyGroup
中实现将Person
归类,与作业2并没有太大改变。
MyNetwork
在本单元是核心类,在本次作业,需要新增很多方法,而这些方法需要许多算法支撑。
比如queruBlockSum
需要用并查集,在前几次作业中,isCircle
方法我是直接用bfs就完成了,但本次作业使用了并查集之后,判断就很快乐。
还有最短路,我用的是普通Diskstra,而没使用堆优化,因此我又为自己的行为付出了代价。
最后就是queryStronglink
,采用了tarjan算法找割点,并维护点双连通分量,最后判断其是否位于同一点双连通分量即可。
但本周老师在课上说的,许多同学只在MyNetwork
就将其代码实现完了,没有什么层次化设计,代码也显得特别臃肿,而我正是这种情况,因为维护了许多容器,导致不好用其他类来管理,我应当优化数据管理,并将算法等以方法来封装,这样才算个好的设计。
同时,在三次作业的迭代过程中,我违反了OCP原则,即经常在之前的代码更改,这就意味着其实在前几次作业的设计中,我并没有做充分的考虑,只把功能实现了了事,而不是优化其做到最好来使得之后的开发收益。
bug分析与修复
本次出现了qmp过多时,因为没使用堆优化而导致的CTLE。
因此在修复bug时,使用了PriorityQueue
。
总结
总的来说,我对本单元不太满意。一个是对自己代码设计出现了过多bug,另一个是对本单元设计的疑问。
先说课程设计吧,在本单元之前,我认为我们的任务是按照JML设计与自己设计JML参半的,结果后来才发现,本单元重心在于按照JML写代码,而设计JML只有实验课涉及了很少。同时由于JML过于详细,限制了我们架构的设计,这也是为什么许多同学都只有四个类MyPerson
,MyGroup
,MyNetWork
,Main
,同时过度关注算法,许多地方很容易出现CTLE,与其是OO课,更像是ds课,这感觉与OO课的初衷背道而驰。
再说自己吧,上面bb了一些,其实感觉自己没有太多资格的,毕竟这单元表现确实不好,第一次作业看着如此多的规格阅读,确实没有太多耐心,因此设计时比较急躁,看错了方法,导致没进互测,非常可惜,同时也没思考好的架构,为后面的失败埋下伏笔。后面作业,虽然改掉了第一次作业不仔细阅读规格的毛病,但又犯了新错误,错误地计算了复杂度,导致了CTLE。总的来说,我在本单元,算法和架构设计都出现问题,实属不该。不过仔细想想,OO这门课作为让我们真正接触设计的一门课,要求的不仅是让我们拿到好的成绩,更是掌握好的设计,如何学习查找资料,改正自己的错误。最后以一句话结尾吧,就当作对自己的勉励:“悟已往之不谏,知来者之可追”。