2019-北航面向对象-JML作业总结

 本次作业的要求为:

针对第三单元的三次作业和课程内容,撰写技术博客

(1)梳理JML语言的理论基础、应用工具链情况

(2)【改为选做,能较为完善地完成的将酌情加分】部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果

    • 有可能要补充JML规格

(3)部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例(简单方法即可,如果依然存在困难的话尽力而为即可,具体见更新通告帖), 并结合规格对生成的测试用例和数据进行简要分析

(4)按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构

(5)按照作业分析代码实现的bug和修复情况

(6)阐述对规格撰写和理解上的心得体会

  第三单元以JML规格为中心的三次作业,对我而言还是有很大难度的。个人感觉,这三次作业与其说是JML规格,不如说是数据结构之图论的诱惑,总是因为莫名其妙的TLECPU超时问题而在强测中爆炸。现在,在第三单元结束后,我将自己在这一单元中所做的工作总结如下:

 

一、 梳理JML语言的理论基础、应用工具链情况

1、理论基础

  JML语言,是用于对Java程序进行规格化设计的一种表示语言,是一种行为接口规格语言(Behavior Interface Specification LanguageBISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML主要由Leavens教授在Larch上的工作,并融入了BetrandMeyerJohn Guttag等人关于Design by Contract的研究成果。近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。

  JML在实际设计中主要有两种用途:

  (1)在实现前对设计的规格进行精确传达

  (2)对已实现的代码进行总结梳理,主要体现在提高代码的可维护性。

2、基础内容

1)注释结构

行注释://@annotation

块注释:/*@ annotation @*/

两种注释方法均适用@开头,并将JML注释放在被注释成分的近邻上部。

2)常见表达式

原子表达式

\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。这里此表达式的类型根据被注释的函数的返回值而定。

\old(expr)表达式:用来表示一个表达式expr在相应方法执行前的取值。当expr有被修改时,使用此表达式,表示expr被修改之前的值。这里有一点需要注意,对于一个引用对象,只能判断引用本身是否发生变化,即只能描述引用对象的地址是否发生变化,而不能描述对象的成员变量等是否发生变化。

\not_assigned(x,y,…)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。

\not_modified(x,y,…)表达式:限制括号中的变量在方法执行期间的取值未发生变化。

\nonnullelements(container)表达式:表示container对象中存储的对象不会有null

②量化表达式

\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。使用的结构如下:

(\forall 类型 变量; 变量满足的限制; 在限制条件下的结果)

以下几种表达式均有类似的使用结构。

\exists表达式:与forall表达式使用结构类似,为存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

\sum表达式:返回给定范围内的表达式的和。

\product表达式:返回给定范围内的表达式的连乘结果。

\max表达式:返回给定范围内的表达式的最大值。

\min表达式:返回给定范围内的表达式的最小值。

\num_of表达式:返回指定变量中满足相应条件的取值个数。

③ 操作符

子类型关系操作符E1<:E2,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1E2是相同的类型,该表达式的结果也为真。

等价关系操作符b_expr1<==>b_expr2或者 b_expr1<=!=>b_expr2,其中b_expr1b_expr2都是布尔表达式,这两个表达式的意思是 b_expr1==b_expr2 或者b_expr1!=b_expr2

推理操作符b_expr1==>b_expr2 或者 b_expr2<==b_expr1,即数理逻辑中的蕴含运算。

变量引用操作符

\nothing 表示空集;

\everything 表示全集。

3)方法规格

前置条件(pre-condition):对调用者的限制,即要求调用者确保条件为真。

后置条件(post-condition):对方法实现者的要求,即方法实现者确保方法执行返回结果一定满足谓词的要求,即确保后置条件为真。

副作用范围限定(side-effects)

assignable 表示可赋值

modifiable 表示可修改

多数情况下二者可以交换使用

④ signals子句

signals子句的结构为signals (Exception e) b_expr,当b_exprtrue时,方法会抛出括号中给出的相应异常esignals_only子句是对signals子句的简化版本,不强调对象状态条件,强调满足前置条件时抛出相应的异常。

4)类型规格

不变式invariant

不变式是要求在所有可见状态下都必须满足的特性。可见状态主要包含以下几种:

对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻

在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻

在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻

在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻

在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻

在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻

状态变化约束constraint

可以认为状态变化约束比上面的不变式放宽了一定的限制,它并不要求一定不发生变化,而是在变化过程中形成要满足一定的约束条件。

3、应用工具链

1openJML

  首先,通过开源的JML编译器(在此处我们选择openJML),编译含有JML标记的代码。可以根据JML对实现进行静态的检查,其中用于进行JML分析的部分在solvers中,包括常见的如cvc4以及z3

  在openJML中,“-check”选项可以对生成的类文件进行JML规范检查。openJML中还包含z3SMT Solver,可以对代码等价性进行验证。通过openJML“-esc”选项对代码的静态验证是不依赖JML的,SMT Solver会自动整理JML

2JMLUnitNG

JML UnitNG可以生成一个Java类文件测试的框架,基于JML并结合Openjml“-rac”选项运行时检查选项,可以根据JML自动生成对应的测试样例,用于进行单元化、自动化测试(实践证明,这种测试主要是边界测试)。

JMLdoc工具与Javadoc工具类似,可在生成的HTML格式文档中包含JML规范。

二、 部署SMT Solver,并选择至少3个主要方法进行验证,报告结果

1SMT Solver

SMT(Staisfiability modulo theories) Solver,是一个定理证明器其工作机理是:openJMLJML规范转换为SMT-LIB格式,并将Java+JML程序所隐含的证明问题传递给后端SMT求解器。

2SMT Solver工作原理

OpenJMLJML规范转换为SMT-LIB格式,并将Java+JML程序所隐含的证明问题传递给后端SMT求解器。

OpenJML支持主要的SMT解决方案,如Z3CVC4YIES

证明的成功将取决于SMT解算器的能力(例如,它支持哪些逻辑)、代码+规范的特定逻辑编码以及代码和规范编写的复杂性和样式。

3、进行检查

1)进行检查的代码

 

(2)格式检查

指令“openjml -check .\Exp\Main.java” 

无误

删除第二行注释末尾的一个分号,再次进行格式检查

报错

 

3)静态检查

指令“openjml -esc -prove .\Exp\Main.java”

报错,部分报错信息如下

 

三、 部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例,结合规格对生成的测试用例和数据进行简要分析

(1) 测试代码

 

(2) 生成测试文件

使用指令“java -jar ..\..\openjml\jmlunitng.jar .\Exp\Main.java”自动生成众多测试文件。对这些文件进行编译,进行运行测试。

(3) 测试结果

 

四、 按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构

1、第9次作业

1)结构分析

 

 

  关于第9次作业,在主类Main中,通过将MyPathMyPathContainer传给AppRunner,实现对这两个功能模块的测试。MyPathContainer类存在对Path类型量的各种操作,因此一定程度上依赖于Path类。

  在MyPath类中,用Arraylisthashmap同时存储一条路径中的所有点。当需要判断是否包含某个点时,用hashmap;当所进行的操作需要对这个路径中所有点按输入时的顺序进行遍历,或者取出按顺序的第i个点,或者对不同的路径进行相同比较、字典序比较时,使用arraylist。同时,因为在MyPathContainer中存在以MyPath类型的变量作hashmapkey值得情况,所以需要重写MyPath类中的equalshashcode

  在MyPathContainer类中,用2HashMap分别存储pathpathidpaths),和pathidpathpathids)的对应关系。还需要一个hashmap(名为distinct)存储各个path中不同的点。名为pathshashmapkey值类型为path,表示路径,value值类型为Integer,表示对应的路径编号;名为pathidshashmapkey值类型为Integer路径编号,value值类型为path,表示对应的路径;名为distincthashmapkey值类型为Integer,表示节点序号,value值类型为Integer,表示对应节点的出现次数。

2)复杂度与依赖度分析

 

这次作业,复杂度较高的主要是MyPath类的compareTo字典序比较方法。但要进行字典序比较,遍历path中所有点在所难免,且这也是强测允许的方法,故我认为它是合理的。

2、第10次作业

1)结构分析

 

 

  我的第10次的oo作业,是在第9次作业的基础上,在第9次作业的基础上增加了MyGraph类的功能而完成的。在这些增加的功能中,比较特殊的是:isConnect()判断fromNodeId的对应点和toNodeId的对应点是否连接,以及getShortestPathLength在相连的情况下求从fromNodeId的对应点到toNodeId的对应点的最短距离。

  这对于这两个功能,我的思路主要是:

  在每次addpath时,除了要更新表示path pathid对应关系的两个hashmap,存储不同节点的distinct结构;还要更新由所有path构成的无向图,清空以前的是否连通”“最短距离储存结果。

  在每次removepath时,同样,在remove pathids paths distinct3hashmap的同时,还要删除无向图中的这个path,同时清空以前的是否连通”“最短距离储存结果。

  在判断是否相连”“求最短距离时,首先查找以前的是否连通”“最短距离储存结果,如果有,直接返回,如果没有,针对无向图graph使用广度遍历算法,并且保存所有中间结果和最终结果。最后,总能判断出是否连通最短距离

   isConnectedgetShortestLength函数中,我的思路是这样的:

  现判断fromNodeId的对应点和toNodeId的对应点是否存在于现有无向图中,如果否,那么抛出异常;如果都存在于现有无向图,那么判断这两点是否是同一点,是否在lengthgraph中已经保存了它们之间的连接关系或最短距离。在这里,如果对应的fromNodeIdtoNodeId存在于lengthgraph中,如果对应数字-1,则两点不相连,否则对应数字就是两点之间的距离。

  对应关系无法在lengthgraph中取得,那么,需要进行一次fromNodeId的对应点到其他能直接或间接相连的所有点的广度优先遍历。我的广度优先遍历,是在《数据结构》课程的广度优先遍历,和CDSN上类似代码分享的基础上完成的,在不停地调用自身中,需要这些参数:

  其中,queue1keylist1的内容相同,表示当前中需要被查找的点,queue2中的点是已经被查找过的点。每次调用这个广度优先遍历函数之前,都需要重新构建符合当前情况的queue1list1queue2

  同时,我对我在MyGraph类中2个双重嵌套Hashmap进行拆分的结果。原本,我在MyGraph类中使用了这样的2Hashmap:

  来表示若干条路径组成的无向图,以及储存每次进行广度遍历的中间结果和最终结果。后来在写代码的过程中,我发现,如果这样设计,每次在graphlengthgraph中进行查找时,都要先通过“.get()”取出整个HashMap<Integer,Integer>,再进行查找;每次对graphlengthgraph中的内容进行增减时都要先通过“.get()”取出整个HashMap<Integer,Integer>,进行增减,再放回原位。我认为这种方式太过笨拙,占用空间也相对大了很多,因此将嵌套在内层的HashMap<Integer,Integer>取出,封装成Line类,并构造相应的一些函数完成查找更改等功能。进行封装后,我的MyGraph类更改为:

 

2)复杂度与依赖度分析

 

  在这次作业中,复杂度较高的部分,主要在于继承自第9次作业的compareTo函数,和本次新增功能的isConnect函数。compareTo函数的遍历path问题无法避免,isConnect函数同样需要进行广度优先遍历。

3、第11次分析

1)结构分析

 

  我的第11次的oo作业,是在第10次作业的基础上,在第10次作业的基础上增加了MyRailwaySystem类的功能而完成的。在这些增加的功能中,比较特殊的是:getConnectBlockCount()判断连通块的数目,isConnect()判断fromNodeId的对应点和toNodeId的对应点是否连接,getShortestPathLength在相连的情况下求从fromNodeId的对应点到toNodeId的对应点的最短距离,getLeastTicketPrice在相连的情况下求从fromNodeId的对应点到toNodeId的对应点的最低票价,getLeastTransPrice在相连的情况下求从fromNodeId的对应点到toNodeId的对应点的最少换乘,getLeastUnpleasentValue在相连的情况下求从fromNodeId的对应点到toNodeId的对应点的最小不满意度。

  相对于第10次作业,我这次作业主要进行了这些设计:

1

  我用Map类来存储彼此不联通的子图。每次新加入一个path,我都首先判断,这个path是需要加入一个已经存在的子图(也就是说,这个path和这个已存在的子图是相连的,有相同的点),还是需要以这个path为基础new一个新的子图。

  如果是加入已有子图中(记为子图A),会不会有不同于子图A的子图B,因这个path而联通在了一起。如果有这样的子图B,那么,需要销毁子图B,将子图B中所有path和对应的pathid,一组一组地加入子图A中。

   每次remove一个path,也都需要销毁这个path所在的子图(记作子图A),再将子图A中不同于path的其他路径,进行加入操作;又因为这些不同于path的其他路径已经有对应的pathid,因此不能直接采用addpath操作,而应该使用相似而不同的insertpath函数。当需要用getConnectBlockCount()判断连通块的数目时,直接返回子图的数量即可。

(2)

  在计算最低票价、最少换乘、最小不满意度时,都涉及到不同path同一站点的换乘问题。我设计了区别于普通nodeNodePathId,用于配合拆点法使用(虽然事实证明拆点法易燃易爆炸)。只有两个NodePathId的点序号node和所在路径序号pathId都相同时,才判断这两点是同一个点。因为存在用NodePathIdhashmapkey的情况,需要重写其equals函数和hashcode函数。

(3)

   在计算最短距离、最低票价、最少换乘、最小不满意度时,我使用的都是迪杰斯特拉算法。但由于在计算最低票价、最少换乘、最小不满意度时,需要考虑不同线路的换乘,需要采用拆点法,因此,我完成了两个迪杰斯特拉算法函数,分别适用于普通node + 最短距离“NodePathId + 最低票价、最少换乘、最小不满意度

   在计算最低票价时,我首先看以往的迪杰斯特拉有没有顺带着得出并保存我想要的结果(正着找一次,反着找一次);如果没有找到,那么,需要进行一次从fromNodeId的对应点到剩下同一连通图中所有点的迪杰斯特拉查找,在此之前,因为我的计算最短距离、最低票价、最少换乘、最小不满意度使用同一个迪杰斯特拉算法,所以我们需要将迪杰斯特拉需要用到的一些参数(在计算最短距离、最低票价、最少换乘、最小不满意度时有所不同的部分)提前算好,传入迪杰斯特拉函数中。

  计算最少换乘、最低不满意度的算法与此大同小异(其实我是复制自己的计算最低票价的函数写的最少换乘、最低不满意度来着)。

  在向Map中加入一个path时,我需要做的事情包括:

  更新用于表示pathpathid对应关系的hashmap,在用于计算最短距离、最低票价、最少换乘、最小不满意度的无向图中讨论是否插入新的(抽象地理解为一个二维矩阵,或者链表);为方便拆点法的使用,构建不同path上相同node的关系(注意,在不同无向图中赋值不同);将新加入的path,一条边一条边地加入;最后,更新某一点都在哪些path中出现hashmap,清空之前所有保存的迪杰斯特拉算法的结果。

 2)复杂度与依赖度分析

 

  在这次作业中,我使用了拆点法+迪杰斯特拉,但由于拆出来的点实在过多,因此,在强测中tle了一些点。改用普通点+弗洛伊德的方法,可以避免tle问题。

五、 按照作业分析代码实现的bug和修复情况

9次作业:

  在MyPath类中,只是用hashmap,无法很好地按存储的顺序取出路径中的所有点,同时在取得过程中可能发现各种错误,所以出现了各种tle问题。在改用arraylisthashmap的结合体后,可以解决所有出现的tle问题。

10次作业:

  无bug.

11次作业:

  在计算最少换乘等问题时,使用了拆点法+迪杰斯特拉,但由于拆出来的点实在过多,因此,在强测中出现了一些tle的点。改用普通点(二维数组储存)+弗洛伊德的方法,可以解决所有出现的tle问题。

六、 阐述对规格撰写和理解上的心得体会

  经过这个单元对JML规格语言的学习,我已经在大体上掌握了这种规格的使用方法。在我看来,这种语言主要是与面向对象的思想相结合,表示希望下一步能得到怎样的状态,而不是下一步具体要进行怎样的动作,即,是状态描述,而不是动作。但是,可能是因为在课下作业中,主要是阅读已经给出的JML规格,写相应的代码,而完成代码部分似乎也与数据结构的图论部分关系更加密切,只是在一次课上完成过写JML规格的练习,所以,我更能阅读JML规格,而相对而言不太能写JML规格,易于存在不完善的地方。

  总体而言,JML语言可以很好的用面向对象的思想描述一个函数的功能,在实际的编程、特别是合作编程中,具有极大的交流合作方面的可操作性,因此其重要性不言而喻,值得拿出一个单元专门训练。

 

 

 

 

 

 

转载于:https://www.cnblogs.com/17373356yc/p/10907939.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值