面向对象编程总结——JML实际应用

一.JML知识梳理

本次面向对象的作业的主要训练内容是将JML在编程中进行实践。体会程序规格在开发中的重要意义。

1.JML语言相关知识

1.1JML语言的理论基础

JAVA 建模语言(JAVA MODELING LANGUAGE,JML),是一种接口行为规范语言。其主要作用是在编写类和方法之前规定好其行为,包括方法的输入数据要求、返回值以及可能会改变的数据。这种建模语言的优势在于可以更加精确的描述JAVA方法的行为,规避了自然语言在描述问题时的二义性,为自动化测试提供了很多方便。但是其缺点在于可读性要明显弱于自然语言,对于一些问题的描述过于复杂,有时会发现对程序的描述甚至比程序还要长很多。相当于对一个项目进行两次编程。

1.2基本语法

(1)JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式

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

(2)requires子句定义该方法的前置条件(precondition)
(3)副作用范围限定,assignable列出这个方法能够修改的类成员属性,\nothing是个关键词,表示这个方法不对
任何成员属性进行修改。
(4)ensures子句定义了后置条件,即largest方法的返回结果等于elements中存储的所有整数中的最大的那个
(\max也是一个关键词)。

原子表达式:

(5)\result表达式:表示一个非void 类型的方法执行所获得的结果,即方法执行后的返回值。

(6)\old( expr )表达式:用来表示一个表达式expr 在相应方法执行前的取值。

(7)\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为
true ,否则返回false 。

(8)\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取
值未发生变化。

(9)\nonnullelements( container )表达式:表示container 对象中存储的对象不会有null ,等价于下面的断言,其中
\forall是JML的关键词,表示针对所有i 。

量化表达式:

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

 \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束

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

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

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

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

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

以下是一个完整的例子。

public class Student {
private /*@ spec_public @*/ String name;
//@ public invariant credits >= 0;
private /*@ spec_public @*/ int credits;
/*@ public invariant credits < 180 ==> !master &&
@ credits >= 180 ==> master;
@*/
private /*@ spec_public @*/ boolean master;
/*@ requires sname != null;
@ assignable \everything;
@ ensures name == sname && credits == 0 && master == false;
@*/
public Student (String sname) {
name = sname;
credits = 0;
master = false;
}
/*@ requires c >= 0;
@ ensures credits == \old(credits) + c;
@ assignable credits, master;
@ ensures (credits > 180) ==> master
@*/
public void addCredits(int c) {
updateCredits(c);
if (credits >= 180) {
changeToMaster();
}
}
/*@ requires c >= 0;
@ ensures credits == \old(credits) + c;
@ assignable credits;
@*/
private void updateCredits(int c) {
credits += c;
}
/*@ requires credits >= 180;
@ ensures master;
@ assignable master;
@*/
private void changeToMaster() {
master = true;
}
/*@ ensures this.name == name;
@ assignable this.name;
@*/
public void setName(String name) {
this.name = name;
}
/*@ ensures \result == name;
@*/
public /*@ pure @*/ String getName() {
return name;
}
}

2.JML应用工具链

2.1 OpenJML

OpenJML可以根据JML对实现进行静态的检查,其中用于进行JML分析的部分在solvers中,常见的包括cvc4以及z3

2.2 JMLUnitNG

可以根据JML自动生成对应的测试样例,进行单元化测试。

二.SMTSolver的验证

1.可满足性模理论 (Satisfiability modulo theories,SMT)

    在计算机科学和数学逻辑中, 可满足性模理论SMT)问题是关于经典一阶逻辑中表达的背景理论与等式相结合的逻辑公式的决策问题。通常在计算机科学中使用的理论的例子是实数理论,整数理论以及诸如列表,阵列,位向量等各种数据结构的理论。SMT可以被认为是一种形式约束满足问题,从而形成约束规划的某种形式化方法。(摘自维基百科)
    说白了就是一种,形式化的推理是否满足既定约束的理论,在计算机科学中相当于形式化分析方法的实现是否满足方法的规格,或者两个函数是不是等价的判断。

2.部署OpenJML以及JML的相关验证

采用z3的证明器进行运算,采用JML在IDEA上的插件进行验证

在IDEA安装完OpenJML的插件之后对Path类进行静态测试发现其中存在二十个警告。警告的大部分内容都是因为官方规格里对于Vector和数组之间的同义性无法下断言。所以将所有的数组变成Vector结构再进行测试。产生了一个警告,因为输入没有对node,length进行限定有可能发生整数溢出产生负数索引值。

Start OpenJML/ESC with file Path.java
D:\test_JML\src\Path.java:17: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method Path
this.nodes.add(nodesList[i]);
^
D:\test_JML\src\Path.java:11: 警告: The prover cannot establish an assertion (Postcondition: D:\test_JML\src\Path.java:10: 注: ) in method Path
public Path(int[] nodesList)
^
D:\test_JML\src\Path.java:10: 警告: Associated declaration: D:\test_JML\src\Path.java:11: 注:
//@ensures (\forall int i; i>=0 && i<nodesList.length;this.nodes.get(i) == nodesList[i]);
^
3 个警告

三.JMLUnitNG的自动生成测试

 为了实践这个自动测试的方法,采用样例的规格文件Sudent.Java使用如下命令生成自动测试文件。

java -jar jmlunitng.jar src/Student.java

 

 

再使用jmlc来编译源文件生成新的class文件。

java -jar D:\openJML\openjml-0.8.42-20190401\openjml.jar -rac Sudent.java

编译完成之后用产生的Path.class 文件替换在production\out\Path.class的文件,运行测试文件发现结果如下

Passed: racEnabled()
Passed: constructor Student(null)
Passed: constructor Student()
Passed: <<Student@400cff1a>>.addCredits(-2147483648)
Passed: <<Student@275710fc>>.addCredits(-2147483648)
Passed: <<Student@55a1c291>>.addCredits(0)
Passed: <<Student@2145433b>>.addCredits(0)
Passed: <<Student@2890c451>>.addCredits(2147483647)
Passed: <<Student@40e6dfe1>>.addCredits(2147483647)
Passed: <<Student@1b083826>>.getName()
Passed: <<Student@105fece7>>.getName()
Passed: <<Student@482cd91f>>.setName(null)
Passed: <<Student@123f1134>>.setName(null)
Passed: <<Student@7d68ef40>>.setName()
Passed: <<Student@1f1c7bf6>>.setName()

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

 

采用上述方法对第一次作业的Path进行测试发现结果如下:

出现错误的位置是因为没有处理Path为空的时候的处理,但是在实验中对空的Path是不会进行任何操作的,在判断isValid()之后就不会再对其成员函数进行调用。

Passed: racEnabled()
Passed: constructor MyPath(null)
Passed: constructor MyPath()
Passed: <<MyPath@400cff1a>>.containsNode(-2147483648)
Passed: <<MyPath@275710fc>>.containsNode(-2147483648)
Passed: <<MyPath@55a1c291>>.containsNode(0)
Failed: <<MyPath@2145433b>>.getNode(0)
Failed: <<MyPath@2890c451>>.getNode(2147483647)
Failed: <<MyPath@40e6dfe1>>.getNode(2147483647)
Passed: <<MyPath@1b083826>>.isValid()
Passed: <<MyPath@105fece7>>.size()
Passed: <<MyPath@482cd91f>>.euqals(null)

===============================================
Command line suite
Total tests run: 15, Failures: 3, Skips: 0
===============================================

 

四. 实验过程中的架构梳理

本次实验分为三个阶段:

1.实现Path接口,PathContainer接口,用于实现路径,和保存路径的数据结构。

2.实现Graph接口,用于对多个路径组成的图结构进行计算的数据结构。

3.实现RailwaySystem接口,将path视为地铁线路,从而对换乘次数,票价,和最舒适路径的规划。

同时数据结构改变次数远小于查询次数,需要对中间结构进行保存来提高运行速度,以下内容将对整个实现过程进行总结。

1.第一次作业

针对Path和PathContainer接口的实现,主要就是增删改查以及偏序接口,整体比较简单。唯一要注意的就是有一个查询容器中的节点计数的方法要注意查询过程应该使用时间复杂度O(1)的算法,我采用了一个HashMap<Node,Integer>的数据结构保存节点以及节点出现在多少个Path中,在每次增删路径的时候维护,查询的时候只要查询表的大小即可。;i类图如下:

 

2.第二次作业

 要求在上次的作业基础上实现图操作,包括是否包含边,是否包含点,是否连通,以及两点之间的最短距离。保存一个距离矩阵作为查询距离和是否连通的依据,每一次图结构变更的时候使用广度优先搜索更新距离矩阵(因为是无权图)。具体的类结构如下。

 基本的结构是继承上一次的PathContainer将path的增删操作进行重写,在不修改上一次的代码的基础上完成了这一次的作业。对于数据的重用:采用上次作业中保存DistinctNode的数据结构在这次用来查询是否包含某个点。

3.第三次作业:

1.要求完成票价查询,票价 = 经过的站点数+2*换乘次数

2.要求完成旅行不满意度查询,一条边的不满意度由相邻两个站点的不满意度的最大值决定,每次换乘会增加32的不满意度

3.要求完成换乘次数的查询。

本质上而言相当于对第二次作业中的图增加权重并且求其最短路径的过程。而对于换乘次数的查询只需要对换乘之间设置换乘代价1,其他的边赋值为0即可通过最短路径算法求出换乘代价。但是相同的Node处在不同的Path的时候会存在换乘的问题,所以为了处理方便,将节点进行拆分,将每个节点分解为(PathId,NodeId)的形式,对NodeId相同的节点之间增加换乘的代价。同时为了查询方便将每个nodeId增加一个源节点作为路径起点的查询,增加一个汇节点作为路径终点的查询。对于一个节点的拆分之后结构如下:

 

对于如图所示的拆点结果,如果查询id1,id2的最短距离,只需要查询(-1, id1)到(0, id2)之间的距离即可,同时由于这两个特殊节点是和其按路径拆点结果单向连接的,对于起点和终点点不是(-1, id1),(0, id2)这两个点的查询不会对换乘产生影响。对于上述拆点的情况,在最差的时候,会导致拆点的图比正常的图多80个点,但是每对最短路径只有其中一个点是有效的。所以在本次作业中Floyd算法时间会远大于缓存实现的Dijkstra算法。对于某些极端的例子,Floyd算法甚至会跑不动。求解最短路径采用Dijkstra算法实现。同时,在数据结构的选择上,采用临界矩阵的方式保存距离值,将不可达的边的值设为100000,通过计算在本次作业中这个数值是无法达到的。对于缓存的设计,包括两部分标志,一个是init,表示图结构是否发生了改变,另一个是update,表示数据是否计算过。每次查询(nodeId1,nodeId2)先查看init是否为真,否则就更新图结构,距离矩阵初始化为新的邻接矩阵。每个节点的更新表boolean update[],update[nodeId1]表示是否更新过此行数据、如果更新过则直接取数值,没有更新过则以nodeId1为原点进行一次Dijkstra运算更新这个点和其他点的最短距离。

程序结构:具体类图如下。

 

 在程序结构上继承上一次的MyGraph类。由MyGraph类对节点之间的连通性进行判断,在连通性成立的情况下在DistanceGraph类中对结果进行运算。对于三种查询只有边的权重不同,所以我选择将所有距离的计算的逻辑封装成DistanceGraph类。同时重写MyGraph中的AddEdge和RemoveEdge方法,增加对DistanceGraph的修改工作。

五. bug的分析和修复

这三次作业在中测强测中都没有发现bug。但是在这次作业中我采用了两步程序的测试过程,第一部分是采用Junit测试工具编写模块化的测试内容,检查基本功能和在正常使用中很难遇到的特殊情况。比如预先确定图结构之后的寻路来测试特定分支。第二步是采用随机生成的测试样例来进行强度测试,测试在高强度的使用中程序的运行速度是否能达到预计要求。

六. 心得体会

  在本次作业的完成过程中我的具体体会到了JML在实际编程中的作用。我认为JML作为一种方法行为描述的形式化语言,具有很好的理论价值。我认为JML语言在对于数据结构成分比较多的编程过程中会有更好的效果,因为对于些类的规定,最主要的就是边界条件的限定,通过这种形式化语言可以很好地帮助我们确定程序的行为,同时减少自然语言的歧义。但是对于实际含义比较强的类比如第三次作业中的地铁问题,规格的实用性变差,因为本身作为一种形式化的语言,其灵活性很差,对于一些使用自然语言很容易描述清楚的问题在使用JML标述时就变得可读性很差。其自动测试工具的使用会很好的帮助我们提高对边界条件的处理。比如很少会考虑到的整数溢出的问题。

我认为本次通过JML的学习让我体会到了一个很重要的观点即在编写程序的时候应该在设计的过程将所有的边界处理都决定好,不要在写程序的时候临时修改函数的接口,这样在单独完成程序的时候还好但是如果涉及到团队开发的时候会导致很多问题,两个人写的程序很有可能会不兼容。

转载于:https://www.cnblogs.com/chenjinyu/p/10898635.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值