OO第三单元JML规格总结分析

OO 第三单元博客作业

1. JML语言概述

1.1 理论基础

  • 综述

Java建模语言(Java Modeling Language,JML)是一种进行详细设计的符号语言,使人们用一种全新的方式来看待Java的类和方法。JML是一种用于Java模块的行为接口规格语言 。JML提供了用于正式描述Java模块行为的语义,从而避免了与模块设计人员意图相关的模糊性。JML继承了Eiffel、Larch和求精演算法的思想,其目标是提供严格的形式语义,同时仍然对任何Java程序员可用。我们可以使用各种工具来使用JML的行为规范,因为规范可以作为注释在Java程序文件中编写,或者存储在单独的规范文件中,所以使用JML规范的Java模块可以不受任何Java编译器的影响进行编译。同时在开发过程中,使用JML语言进行建模可以帮助我们跳出控制语句的约束,只从功能的角度给出类或方法的期望作用,内部数据的期望变化而不去考虑用什么方式来实现这种变化。通过这种抽象,我们在设计时就会减轻很多压力而专注于设计一个有好的功用的结构框架以供开发,提高效率。

  • JML表达式

    • 原子表达式:见后文引用。
    • 量化表达式:见后文引用。
    • 集合表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
  • 方法规格

    • 执行前对输入的要求----前置条件(requires)
    • 执行后返回结果应该满足的约束----后置条件(ensures)
    • 副作用范围限定,assignable列出这个方法能够修改的类成员属性
    • 规格中专门说明exceptional_behavior
  • 类型规格

    • 数据状态应该满足的要求----不变式(invariant)
    • 数据状态变化应该满足的要求----约束(constraint)

原子表达式

  • \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对应的准确类型。

量化表达式

  • \forall表达式:全称量词修饰的表达式。
  • \exists表达式:存在量词修饰的表达式。
  • \sum表达式:返回给定范围内的表达式的和。
  • \product表达式:返回给定范围内的表达式的连乘结果。
  • \max表达式:返回给定范围内的表达式的最大值。
  • \min表达式:返回给定范围内的表达式的最小值。
  • \num_of表达式:返回指定变量中满足相应条件的取值个数。

1.2 应用工具链

JML相应的工具链,可以自动识别和分析处理JML 规格。

在规格语法上,有和Javadoc相类似的JMLdoc工具,可快速生成相关文档和文件。

常用的静态检查是openjml,在后端使用SMT Solver来对检查程序实现是否满足所设计的规格(specification)。目前openjml封装了四个主流的solver:z3, cvc4, simplify, yices2。

动态检查有诸如jmlrac这样在动态编译时判断JML规格是否满足要求。

自动测试有诸如JMLUnit(NG)一类的应用,可以根据编写的规格自动生成测试文件,再经过jml编译后通过TestNG来自动生成测试文件,便于模块化测试。

2. SMT Solver 部署

SMT Solver为OpenJML提供了语法树检查的后端, 下尝试基于Win10 + JDK1.8.0

按照讨论区的大佬提供的教程就可以愉快地在命令行食用openjml+smt solver啦

本次采用对JML的语法检查,静态检查以及动态检查:

openjml -check <source files> 
openjml -esc <source files> -exec Solvers-windows\z3-4.7.1.exe
openjml -rac <source files> -exec Solvers-windows\z3-4.7.1.exe

其中语法检查就是对JML规格的检查,本次选用的第9次作业的MyPath没什么错误,而动态检查的时候按照讨论区的方法总是找不到主类,故先不做讨论。(测试用例和下文JMLUnitNG中用例相同)

运行后结果为:

D:\openjml>java -jar openjml.jar -esc src\MyPath.java -exec Solvers-windows\z3-4.7.1.exe
src\MyPath.java:9: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method MyPath
            this.nodes[i] = nodeList[i];
                      ^
1 个警告

可见SMT给出了数组下标可能为负数的警告。

再对第三次作业我自己自建的CstmGraph类进行检测,结果如下(只截取部分有意义的):

src1\CstmGraph.java:133: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method computeUns:  underflow in int difference
        return leastSfDist[from][to] - 2 * halfUs;
                                     ^
src1\CstmGraph.java:162: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method dijkstra
                    dist[id] = dist[curNode.getId()] + cost;
                                                     ^
src1\CstmGraph.java:84: 警告: The prover cannot establish an assertion (InvariantLeaveCaller: openjml.jar(specs/java/util/Map.jml):76: 注: ) in method updateCstmGraph:  (Caller: com.calvin.oohw11.CstmGraph.updateCstmGraph(java.util.HashMap<com.oocourse.specs3.models.Path,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.Integer>), Callee: java.util.HashMap.containsKey(java.lang.Object))
                    if (!tmpPathMap.containsKey(bgN)) {
                                               ^
openjml.jar(specs/java/util/Map.jml):76: 警告: Associated declaration: src1\CstmGraph.java:84: 注:
        public invariant content.owner == this;
               ^

上述代表性的检查检查了整数上界和下界溢出,数组上下界溢出以及维护多个map的时候的key和value的不变式在增删的时候不满足而报错,大体上报错种类就是这些。

3. JMLUnitNG测试

同样基于讨论区大佬的教程,采用win10+JDK1.8.0,进行命令行操作。

由于Graph的接口过于庞杂,同时穿插有大量的\exists\forall,故在测试的时候选取一个较简单的类--Path类,同时将里面的/sum等不支持语法进行修改,删去了一些函数,得到如下形式的MyPath:

package src;

public class MyPath {
    private /*@ spec_public @*/ int[] nodes;
    
    public MyPath(int[] nodeList) {
		nodes = new int[nodeList.length];
        for (int i = 0; i < nodeList.length; i++) {
            this.nodes[i] = nodeList[i];
		}
    }
    
	//@ assignable \nothing;
	//@ ensures \result == nodes;
    public /*@pure@*/ int[] getNodes() {
        return nodes;
    }
   
    //@ ensures \result == nodes.length;
    public /*@pure@*/ int size() {
        return nodes.length;
    }
    
	/*@ requires index >= 0 && index < size();
	  @ assignable \nothing;
	  @ ensures \result == nodes[index];
	  @*/
    public /*@pure@*/ int getNode(int index) {
        if (index >= 0 && index < nodes.length) {
            return nodes[index];
        } else {
            return -1;
        }
    }
  
    /*@ ensures \result == (\num_of int i, j; 0 <= i && i < j && j < nodes.length;
                             nodes[i] != nodes[j]);
	@*/
    public /*@pure@*/ int getDistinctNodeCount() {
        return nodes.length;
    }
    
    
    //@ ensures \result == (nodes.length >= 2);
    public /*@pure@*/ boolean isValid() {
        return nodes.length >= 2;
    }
	
	public static void main(String args[]) {
        return;
    }
}

接下来按照讨论区的说明一步步运行

jmluniting src/MyPath.java
javac -cp jmlunitng.jar src\*\.java
openjml -rac src\MyPath,java

运行到此处有一个报错:

src\MyPath.java:36: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression
    /*@ ensures \result == (\num_of int i, j; 0 <= i && i < j && j < nodes.length;

经检查是自己将getDistinctNodeCount的\num_of规格不支持,555

接着运行:

javac -cp jmlunitng.jar yifan/MyPath_InstanceStrategy.java  
java -cp jmlunitng.jar yifan.MyPath_JML_Test

最终得到结果

[TestNG] Running:
  Command line suite

Passed: racEnabled()
Failed: constructor MyPath(null)
Passed: constructor MyPath({})
Failed: <<src.MyPath@3f102e87>>.getDistinctNodeCount()
Passed: <<src.MyPath@6fdb1f78>>.getNode(-2147483648)
Passed: <<src.MyPath@51016012>>.getNode(0)
Passed: <<src.MyPath@29444d75>>.getNode(2147483647)
Passed: <<src.MyPath@2280cdac>>.getNodes()
Passed: <<src.MyPath@4fccd51b>>.isValid()
Passed: <<src.MyPath@44e81672>>.size()
Passed: static main(null)

===============================================
Command line suite
Total tests run: 11, Failures: 2, Skips: 0
===============================================

两个fail的点一个是传入MyPath的nodeList为空时的构造函数,这是调用nodeList.length会报错;

getDistinceNodeCount由于无法满足\num_of的规格,故也会报错。

4. 架构设计及迭代时的重构

4.1 第一次作业

  • 架构设计

第一次的作业十分简单,基本上只要设计好数据结构然后按照规格的方法来就可以了。在本次作业中我只是设了两个官方要求的类:MyPath和MyPathContainer,MyPathContainer会调用MyPath里面的内容进行Path的建立。

MyPath中,我是用了Arraylist来存储每个路径的节点序列,因为结点序列要求既可重复有可被比较。在实现Path的compare接口时,我自己写的Hashcode的方法只是利用了数组的长度和首尾元素的值的线性组合,这样的Hashcode几乎就和没写一样,导致时间复杂度很高。

在MyPathContainer中,我建立了两个hashMap,一个是pathId -> Path, 另一个是path -> pathId并把它们当做一个HashMap使用,这样contains的问题就都可以转到key的contains问题上。同时remove和add操作都需要进行两个HashMap的同步操作以防数据不一致。在对getDistinctNodeCount的操作时,笔者没有将这个的复杂度分摊到所有的线性操作中,而是每一次尝试合并不同Path中的distinctNode,这样也带来了时间复杂度上面的隐患。

现在想想应该在第一次作业就抽象出点的模型,像标程一样

4.2 第二次作业

  • 架构设计

第二次作业是在第一次作业的基础上实现一个有关路径的无向图结构,并实现简单的对图的边与点的查找,连通性的判断以及对图上任意两点的最短路径查找。

这次我没有对MyPath以及MyPathContainer进行重构,只是将MyGraph继承自MyPathContainer然后对其中的addPath,removePath以及removePathById进行了扩展。这次的图结构改变指令被限制在了20条指令一下,这种情况就必须将那些查询操作分摊在这些建图拆图的操作中,这样就可以将时间复杂度降下来。本次作业笔者就采用了这种方法。

在数据结构方面,笔者首先将Path里面的稀疏点密集化,由于同时出现在图中的结点最多只有250个,笔者就把他们映射到1-250这250个图上结点,映射的结构用一个进行表示,并设置了两个 unusedNodeusedNode进行维护,在程序中时刻保证两个集合的交集为空,并集为1-250所有点集。同时笔者为统计不同path中相同结点的个数专门设立了graphNodeCount数组进行统计,以便remove在移除path时决定该path上面的node是否需要进行移除。对图的表示方面,笔者采用了可达矩阵距离矩阵相结合的方式,在实际操作时先依据结点的增删情况用linkEdgesInPath/deLinkEdgesInPath更新增删path上的结点连通性,之后更新可达矩阵,最后调用updateShortestPath,调用Floyd算法来更新所有点的最短路径。

4.3 第三次作业

  • 架构设计

本次作业是将第二次作业的无向图具体化为地铁线路图,然后根据不同的需求(连通性,不满意度,最少换乘,最少票价)进行建图,最短路查找。难点就在于如何将这些需求归类,然后用尽可能统一的方法进行问题解决。在经过与同学的讨论后,笔者决定对第二次Graph的结构进行一些重构以便更好支持第三次作业的结构。

笔者将第二次作业中图结构的表示方法由可达矩阵和距离矩阵换为了基于path的邻接链表以及距离矩阵 表示,记为linkednode。同时维护了一个从path里面原有的稀疏node到Graph的密集node的,还有一个将每个Graph的点映射到该点所在所有的path的。同时每次进行addPath和removePath相关的操作时,都会按照每个path的情况更新上述三个结构以及重建距离矩阵。这里的更新采用重建的方式,因为分摊所有操作时复杂度并不高,同时重建的方式会省去很多逻辑上的判断处理。

重构之后便是新的数据结构的添加。其中最大连通情况的判断被笔者整合到了找寻平凡的最短路径的过程中。笔者采用bfs的方式在搜寻最短路的同时记录每个结点能够连通的最大区域,这样就一举两得。对于最小换乘次数,笔者在与同学讨论后发现可以从原有的路线图将每条路线抽象成为点,这样每两个点之间的连线就代表这两条路线可以换成,把所有满足条件的路线之间连接起来就可以通过在抽象出来的图上面跑bfs来进行最小换成次数的查询。

最小不满意度和最小票价的问题可以抽象为具有不同权值的同一个图。具体说来就是两个图都需要将换乘点拆开,然后将票价或者不满意度赋给拆出的点的连接边。对每个结点车站(不是只有换乘点),我们抽象出一个站台的概念,这个站台会连接到任何会途径此站的结点,但是不会连接到任何一个站台。这样在计算上述问题时,所有人都是先从某个站点的站台出发,先到想乘坐地铁的结点处上车,然后有换乘就在换乘点下车,仍然先到站台,再经提示到换成地铁的结点处乘下一班地铁,出站时也要先从结点走到站台再出站。这个结构的好处便是统一了换乘点与非换乘点的结构,使得在进行最短路径的查询时更易编程实现。注意到换乘的概念被拆解为先去站台再去换乘结点,所以换乘的边权就很直观地一分为二,一半给去站台,一半给出站台,这样整个结构就构建完成,对每个结点跑dijkstra算法即可求出带权最短路。这里还要注意的由于初始站点和结束站点实际上并不需要有边权,所以最后跑完dij减去2倍减半的换乘边权即可(2 * 1, 2 * 16)。

5. bug和修复情况

  • 第一次作业由于自己的Path在比较时的hashcode没有继承Arraylist自己的hashcode,而是自己写的hashcode,导致比较的时间复杂度回到了O(n),同时自己没有对统计结点个数这个操作将时间分摊到复杂度无法降低的增删操作上,最终TLE了四个点。
  • 第二次作业由于自己没有及时更新规格,在单个点是否连通问题上出错,导致wrong了一半的点(当时就觉得规格写得有点奇怪。虽然没及时看通知改规格是我自食其果,但也希望助教高工助教大大在微信群里面提醒一下。
  • 第三次作业应该是直接挂掉了,,, 因为是自己把时间留给了另一个自己认为更重要的事情上,所以也没什么感到遗憾的(人生有得有失吗)。虽然笔者觉得算法在与同学讨论后比较清晰,但是实现时写得过于匆忙导致手忙脚乱,最后根本没有进行全面测试就到了截止时间。在后续的debug发现是自己不同层级的结点(path里的结点,graph里的结点,再加入换乘点之后的结点以及抽象出的path图的结点)之间映射关系搞混了,最后导致了本该都为正的graph结点以及换乘结点中出现了为负的path结点导致抛出OutOfBound错误。这次的教训应该就是还是要提前把握好每次作业的难度,之后在权衡一下时间投入。

6. 规格撰写和理解体会

这一单元在接触到规格撰写的时候自己还有些摸不着头脑。在第一节课老师举例子将一个用语言描述的话转化成规格语言的时候我的第一反应是:写个注释干嘛这么非人类!!! 这种规格一点都不显然好吗?后来又有一种感觉:怎么这么像离散数学一里面的形式化证明的感觉(后知后觉说明离散一也没学好... 。在进行了几次作业之后发现了规格他自己的妙处。对比我们自己写注释的时候,我们总是按照程序的进行一条一条写出来,包括这里干嘛要设计一个HashMap<Integer, Path>,每一个部分都代表什么,还有这个if分支控制的是什么... 我们的注释就这样成为了唯我独尊的注释,只是成为了解释我自己写的代码的工具,阅读注释的人只能通过看一句程序看一句注释的方式对你的代码有一个大致的了解,然后自己的脑袋中再抽象出你这段代码的功能。由于这种主观性与限制性,这种方式就使得代码理解与交流会成为阻碍工程开发的一环。

而规格化语言JML要求架构者跳出程序控制的限制,跳出数据结构的约束,单单从方法或类的功能进行阐述并将它传递给后续阅读程序的人或是功能的实现者。这样做有两个好处:首先它保有了设计者在设计架构时的最初想法,从而不需要后续阅读者从分散的注释去揣测架构者的原始意图,大大增加了开发效率;同时这种规范化的语言便于检查正确性,同时便于测试者进行测试:首先这种规范的语言由于符合基本逻辑表述,可以被一些数学逻辑检查工具直接验证从而判断覆盖性与正确性,同时在保证正确性的前提下测试者在编写测试用例的时候可以分条验证从而也保证测试的良好覆盖性,大大加速工程的开发与测试过程。

在这三次规格作业后,我也初识规格的巨大威力所在,在今后的学习工作生涯中,我也会不断提升自己撰写规格,抽象程序的能力,继续进步!!

posted @ 2019-05-22 17:14  CalvinL  阅读(345)  评论(0编辑  收藏  举报