JML 规格化设计总结

JML 规格化设计总结

​ 我们进入了规格化设计单元——JML(Java Modeling Language)。本单元整体感受要比之前两个单元的工程量小了很多,但是不要小看了JML的威力(还是很容易出错的....对离散数学以及数据结构的基础知识要进行回顾)。

​ 本单元要实现从程序的设计者角度,向程序实现者角度的转变。在理解架构的基础上,满足契约,进行自己的模块算法的设计。规格化设计在工程实现领域还是有很重要的地位的。掌握规格化设计能够为我们以后在工作岗位团队协作,提高代码质量带来很多便利。下面我将对本单元作业进行梳理。

JML理论知识与工具链

JML理论知识

下面我对这三次作业、两次实验多次出现最精简的JML理论知识进行总结。详细的内容可以参考课程组下发的课程学习ppt和JML Level0手册

1.注释结构

行注释的表示方式为 //@annotation ,块注释的方式为/* @ annotation @*/

2.JML表达式

原子表达式

  • \result:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
  • \old( expr ):用来表示一个表达式 expr 在相应方法执行前的取值。
  • \not_assigned(x,y,...):用来表示括号中的变量是否在方法执行过程中被赋值。

量化表达式

  • \forall:(对应数学中的任意符号)全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
  • \exists:(对应数学中的存在符号)存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
  • \sum:返回给定范围内的表达式的和。
  • \max:返回给定范围内的表达式的最大值。

操作符

  • 推理操作符:==>

3.方法规格

  • 前置条件(pre-condition):requires,进入类或方法的参数应该满足的要求
  • 后置条件(post-condition):ensures,经过类或方法处理后的变量有何变化
  • 副作用范围限定(side-effects):assignable,此类或方法引起的变化
  • signals子句:signals,对应于exception_behavior,抛出异常
  • 当然设计中也常用pure关键词,表示纯粹访问的功能方法

4.类型规格

  • 不变式invariant:在所有可见状态下都必须满足的特性(白话就是不用再在每个方法的规格里面都对变量ensures了。避免了在方法中对对象造成改变。)
  • 状态变化约束constraint:对变量的变化进行约束。(侧重于变化)

JML工具链

“工欲善其事,必先利其器”

  1. OpenJML

    OpenJML最基本的功能就是对JML注释的完整性进行检查。检查包括经典的类型检查、变量可见性与可写性等。通过命令行使用OpenJML时,可以通过-check参数(缺省)指定类型检查。

    点此下载

  2. JMLUnitNG

    配合上面说到的OpenJML可以根据JML自动生成TestNG测试文件的工具

    点此下载

  3. SMT Solver

    用来证明代码逻辑等价的,也就是可以用来从形式上证明两个函数的效果是等价的

OpenJML与JMLUnitNG实现自动生成测试用例

openJML部署与规格静态检验

目录结构如下:

cmd命令如下

java -jar openjml的路径 -exec 解释器的位置 -esc 需要验证规格的文件的路径

对Person.java进行分析通过测试

下面我们再举一个比较常见的例子:

public class testJML{
    public static void main(String[] args) {
        testJML testjml = new testJML();
    }
    
    //@ensures \result == a * b;
    public int mul(int a, int b) {
        return a * b;
    }
    
    //@ensures \result == a / b;
    public int div(int a, int b) {
        return a / b;
    }
    
    //@ensures \result == a % b;
    public int mod(int a, int b) {
        return a % b;
    }
}

检验结果出现除0,模0以及乘法大小限制的警告。

JMLUnitNG自动生成测试

此处为jyc大佬博客的链接

依次输入以下指令:

java -jar jmlunitng-1_4.jar test\MyGroup.java

值得提出的是,在IDEA中我们经常缺省,但是在检验的时候会出现error。所以要把泛型补全。

javac -cp jmlunitng-1_4.jar test\*.java

生成一堆.class文件

java -cp jmlunitng-1_4.jar test.MyGroup_JML_Test

自动测试效果还是非常好的,自动生成的数据大部分是一些边缘数据。我们可以看一下Failed的点,是addPerson和delPerson,原因是在规格中没有给这两个方法设定限制条件。限制条件在NetWork.java中,本次没有测试,所以会出现null的错误。

架构设计及bug分析

第一次作业

第一次作业我按照代码仓库给出的代码架构框架进行设计。如下图所示:

bug分析

第一次作业整体上比较简单。但是,我却出现了大量的CTLE。问题出现在isCircle方法,即判断两点是否连通的处理上。我使用了DFS算法,以下为错误示例:

// 关注对path的操作
	path.put(indexPerson.getId(), indexPerson);		// 进入函数入栈
    if (indexPerson.equals(aimPerson)) {
        HashMap<Integer, Person> element = new HashMap<>(path);
        paths.add(element);
        path.remove(indexPerson.getId());		// 出栈
        return true;
    } else {
        HashMap<Integer, Person> acquaintance = ((MyPerson) indexPerson).getAcquaintance();
        Iterator<Map.Entry<Integer, Person>> iterator = acquaintance.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, Person> entry = iterator.next();
            if ((path.get(entry.getKey()) == null)) {
                flag |= Dfs(entry.getValue(), aimPerson);
            }
        }
        path.remove(indexPerson.getId());	// 出栈
        return flag;
    }
}

上方DFS代码看似正确(我最初也一直是这样认为的),但是却存在致命的问题——$O(n!)$ 复杂度。举一个简单的例子,如下图:

我想判断1到5是否连通,那如果我按照上方代码进行寻找的话,我找到1234,1243,1324,1342,1423,1432,15。很明显可以看出进行了许多无效查找。仅仅是一个简单的图就已经做了许多无效查找,如果图的复杂度更高,超时也不是冤枉。这说明仅仅维护栈是不够的。

其实优化也很简单,就是做一个visit数组,保存已经访问过的顶点即可。这样遍历就变成了1234,15。变回了我们的老朋友——“善良的”DFS

关于找到别人的Bug,由于我进入的是C屋,基本上随机生成数据就有很好的hack效果.....同屋子的同学基本都是超时问题,我研究他们的代码,很多isCircle和我设计的思想完全一样。(说明这个问题其实也属于很多基础不扎实的同学的共性)

第二次作业

第二次作业加入了Group,即关系群。

我是按照给出的框架填充的。这次作业比较简单,重点是我理解到不可以仅仅是按照JML规格给的格式设计。如下图:

如此简洁美丽的注释,按照注释写不就行了?

那恭喜你!超时了!

本次作业只要用到缓存数据的思想即可——在数值改变的时候,对数值进行更新;读取的时候就可以达到$O(1)$的复杂度。

因为迭代开发,代码量比较小。并未发现自己与别人的bug。

第三次作业

第三次作业果不其然要考到图算法。本次涉及到了:最短路径问题、双向联通问题以及连通块个数问题。

最短路径问题我应用了dijkstra算法。为此我多设置了一个Item类,即PriorityQueue存放的对象。Java已经自带PriorityQueue方法,用起来很方便,只需要自定义以下比较规则就可以了。

双向联通最好的解决方法是tarjan(塔扬)算法。但是由于听闻比较容易出错,我应用了暴力求解算法...然后超时了。优化后采用了两次DFS,每次记录下连通的路径个数。对路径个数进行判断。选取第一次遍历的通路中最短的,对每个点分别进行Mask,再次DFS,如果仍然可以连通,那就可以了。一定注意分别对每个点mask!

连通块的个数可以在ap是+1,ar时如果!isCircle,就-1。同时把isCircle改为并查集。(还可以用缓存的思想进一步优化,回想以下计组的dirty)

hzx大佬关于算法复杂度的分析

本次还是出现了超时错误。确实没想到强测会是举基本所有3000条指令测特定的指令orz。互测的时候我们屋非常安静....活跃分扬了...

心得体会

丰富的心理变化:

OO为什么突然这么简单了???简直不可思议!!!中测一遍过???

原来不是我变强了,是中测变弱了,满屏的超时....甚是害怕

提心吊胆搞优化,明白了不能仅仅按照JML写的去实现。JML只是为我们提供了一个输入输出的契约,但是算法与数据结构还是要自己去选择。

OO课程也过去了一大半了,觉得这门课程虽然比较难,但是确实还是很有收获的。

在不断优化的过程中,对容器的了解也进一步加深。HashMap这种比较快的容器,可以在恰当的时候多使用。每种容器都有自己的适用条件与优缺点,不是ArrayList解决一切。

JML规格可以规范自己的设计,要认真读懂,关注细节,但具体实现还是要靠自己的理解。不要逐字逐句去翻译。

最后,行百里者半九十,认真对待最后一个单元!

猜你喜欢

转载自www.cnblogs.com/zhangjiayuan/p/12941300.html