JML契约式设计——第三单元学习小结
一、前言
本单元作业都是关于JML(Java Modeling Language),JML是一种契约式设计(Design by Contract)的语言,契约式设计的主要目的是希望程序员能够在设计程序时明确地规定一个模块单元(具体到面向对象,就是一个类的实例)在调用某个操作前后应当属于何种状态,它强调三个概念:前置条件,后置条件和不变式,要求输入的参数满足前置条件,要求函数完成时的状态满足后置条件,要求函数开始运行和结束运行时满足不变式,即对调用者来说不变式总是为真,而对函数内部来说不变式可以为假。对JML来说,前置条件就是requires限定的条件,后置条件就是ensure限定的条件,不变式则是invariant限定的条件,其他的语句像assignable等都是为这几个条件服务的。
本次作业即是基于JML,给出程序运行框架,把几个方法“挖空”,然后给出这几个方法的JML规格,让我们“按图索骥”。只是这“骥”是千里马还是下等马,还是要靠自己的代码功底及完善的单元测试决定。本次作业用到的工具有:idea, jdk, Junit, OpenJML, JMLUnitNG,SMT Solver等。
二、JMLUnitNG的使用
经过一下午的尝试,发现JMLUnitNG确实有点不好用,也可能是我不会用,他连一些新写法的JAVA语法都检查不通过,因此我只好写了个简单的测试文件:
import java.util.ArrayList; public class JMLUnitNG {
public ArrayList<Integer> array = new ArrayList<Integer>(); public JMLUnitNG() {
/*array.add(12);
array.add(23);*/
} public void addIncrease(int ele) {
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > ele) {
array.add(i, ele);
}
} } public void remove() {
array.remove(0);
} public int addFirstAndSecond() {
return array.get(0) + array.get(1);
} public int multFirstAndSecond() {
return array.get(0) * array.get(1);
} public static void main(String[] args) {
JMLUnitNG unit = new JMLUnitNG();
}
}
得到测试结果:
Failed: racEnabled()
Passed: constructor JMLUnitNG()
Failed: <<JMLUnitNG@6fdb1f78>>.addFirstAndSecond()
Passed: <<JMLUnitNG@4fccd51b>>.addIncrease(-2147483648)
Passed: <<JMLUnitNG@4ca8195f>>.addIncrease(0)
Passed: <<JMLUnitNG@65e579dc>>.addIncrease(2147483647)
Failed: <<JMLUnitNG@61baa894>>.multFirstAndSecond()
Failed: <<JMLUnitNG@b065c63>>.remove()
Passed: static main(null)
Passed: static main({}) ===============================================
Command line suite
Total tests run: 10, Failures: 4, Skips: 0
===============================================
但他的测试数据也太简单了吧,基本没啥用,并且当我把构造方法的注释取消以后,连结果都运行不出来了,只有如下三行:
Failed: racEnabled()
Passed: constructor JMLUnitNG()
Passed: <<JMLUnitNG@5f5a92bb>>.addFirstAndSecond()
三、SMT Solver
测试代码:
public class Path {
public int[] 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) {
return nodes[index];
} /*@ public normal_behaviour
@ ensures \result == nodes[i] - nodes[j];
*/
public int compare(int i, int j) {
return nodes[i] - nodes[j];
} public static void main(String[] args) { }
}
然后 运行命令行:
java -jar .\openjml.jar -exec C:\Users\yang\Desktop\\openjml-0.8.-\Solvers-windows\z3-4.7..exe -esc C:\Users\yang\Desktop\\openjml-0.8.-\Path.java
得到结果:
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:4: 警告: The prover cannot establish an assertion (NullField) in method Path
public int[] nodes;
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method compare: underflow in int difference
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method compare
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method compare
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method compare: overflow in int difference
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (Postcondition: C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:22: 注: ) in method compare
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:22: 警告: Associated declaration: C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 注:
@ ensures \result == nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method compare
return nodes[i] - nodes[j];
^
C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java:25: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method compare
return nodes[i] - nodes[j];
^
9 个警告
可以看出大部分都是溢出警告,改了以后就没有警告了。
然后运行rac检查:
java -jar .\openjml.jar -exec C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Solvers-windows\z3-4.7.1.exe -rac C:\Users\yang\Desktop\1234\openjml-0.8.42-20190401\Path.java
并未发生警告。
四、设计思路
第一次作业的思路很简单,分成两个类:Path和PathContainer,对于Path,我们要做的是查询Path的第i个元素,以及查询Path的不同的元素的个数,查询是否包含某个元素。在这里我们可以直接选择ArrayList把这些功能都完成,但是对于我们来说,速度也是很重要的,于是我们设计了两个容器,ArrayList和HashSet, ArrayList按顺序存放Path的元素,按索引查询就用这个,然后HashSet是存不同的元素的,将每一个元素加入HashSet,然后HashSet的size()方法返回值即是不同元素的个数,查询Path是否包含某元素也可直接用HashSet的contains方法查询。对于PathContainer,我们要做的是对完成一个Path的容器,实现增删改查的操作,用ArraryList容易超时,于是我选择了HashMap,将每条Path加入HashMap中,加路径,删路径,直接可以访问HashMap,时间复杂度很低。但是怎么查询Path呢,用ArrayList不是不可以,只是在执行删除操作以后不好处理其中的元素,于是我再建了一个HashMap将路径序号映射到Path,这样两个HashMap共同使用就可以完成这个容器类了。
第二次作业则变成了OO数据结构复习课,将我们的数据结构的图论知识复习了一遍,本次作业的需求是在第一次作业的基础上加了节点之间的最短距离,这就要用到Dijkstra或者floyd等算法了,然后我选择了floyd算法,首先floyd只需要在加或者删的时候计算一遍就OK,纵使他的复杂度有O(n^3),我仍然觉得他在多条查询最短距离的情况下会比Dijkstra快,其次,floyd只需计算一次,就能将所有节点间的最短距离保存下来,之后查询就不需要耗费时间了,我觉得这更符合人的思维习惯。第三,floyd简单易懂,扩展性强。但是节点的编号可以是整个int的范围,这个用邻接矩阵必定会MLE,所以我们将节点编号映射到0-250的范围,再开一个250*250的矩阵,套用flyod算法,直接开花,直接得出结果。
第三次作业则是本次作业难度的分水岭,它加四个需求,最低票价,最少换乘次数,最少不满意度,以及连通块数量。这次在同学间在最低票价,最少换乘次数,最少不满意度的计算中就出现了Floyd和Dijkstra两大教派了,Floyd通过特殊的矩阵可以直接算出这三者,而Dijkstra可以通过拆点,缓存的手段,在每次查询中都计算一遍节点到节点的最低票价,最少换乘次数,最少不满意度以及最短距离,并缓存下来。这种方法听起来就很复杂,既然追求速度和简洁,那就要贯彻到底,于是我依旧是用了floyd算法。本次作业的floyd算法使用的矩阵是特殊的:
- 对于最少换乘次数,将每条路径内每两个节点之间的距离设为1,如图:
加入两条路径:1-2-3和4-1-3-5
第一步,将1-2-3路径的每两个点的距离设为1:
第二步:将4-1-3-5的每两个点的距离设为1:
第三步,合并两路径的图:
在这个图中计算最少换乘次数就是计算每两个点间的距离再减个1,比如2-3间距离为1,减一就是0,则表示这两个点不需要换乘。2-5距离为2,减一就是1,表示这两个点之间需要换乘1次。
- 对于最低票价,和最少换乘次数想法相仿,就是先将每条路径的每两个点之间的最低票价算出来,将这些值+2后填入矩阵(若有两点间有多条路径,取最低票价),然后再对整个矩阵进行floyd,每两个点之间的最低票价为对应的矩阵值-2。因为每条路径都可能有环路,所以求每条路径的最短票价时也需要用一次floyd。
- 对于最少不满意度,同最低票价,将每条路径每两个点之间的最少不满意度有用floyd算出来,将这些值+32后填入矩阵(若有两点间有多条路径,取最少不满意度),然后再对整个矩阵进行floyd,每两个点之间的最少不满意度为对应的矩阵值-32。
- 优化:因为这些矩阵在每一次add或者remove的时候都要更新,如果要把所有路径各自的矩阵全算一遍,然后再把这些矩阵结合起来再算一遍总的最低票价,最少不满意度,这个复杂度会特别高,于是我修改了Path类,将单条路径的不满意度和最低票价存起来,然后设置了一个values类,将每条两个点间的不满意度和最低票价以升序方式存入:
1 public class Values {
2 private LinkedList<Integer> valueList = new LinkedList<>();
3 private static int MAX = 0xfffffff;
4
5 public Values() {
6 valueList.addFirst(MAX);
7 }
8
9
10 public void insert(int e) {
11 for (Integer tmp : valueList) {
12 if (e < tmp) {
13 valueList.add(valueList.indexOf(tmp),e);
14 return;
15 }
16 }
17 }
18
19 public int getFirst() {
20 return valueList.getFirst();
21 }
22
23 public void remove(int e) {
24 for (Integer tmp : valueList) {
25 if (e == tmp) {
26 valueList.remove(tmp);
27 return;
28 }
29 }
30 }
31 }
连通块数量的计算方法我并没有采用网上说的并查集或者bfs算法,因为经过我的观察,只要有邻接矩阵,就能直接用二重遍历的方式查找出连通块数量,并且速度不比上述两种算法慢:
public void calConnectNum() {
int count = 0;
int[] union = new int[MAXNODE];
for (int i = 0; i < MAXNODE; i++) {
if (union[i] > 0) {
continue;
}
boolean flag = false;
for (int j = i; j < MAXNODE; j++) {
if (i == j) {
if (adjMatrix[i][j] > 0) {
union[j] = count + 1;
flag = true;
}
}
else {
if (dstMatrix[i][j] < MAXDST) {
union[j] = count + 1;
flag = true;
}
}
}
if (flag) {
count++;
}
}
connectBlockNum = count;
}
五、代码分析
UML类图:
从左至右分别是第1到3次作业:
可以看到第一次和第二次依赖关系很简单,第二次只是加了几个方法而已。第三次作业加了一个Values类,增加了一些方法,依赖关系也并不复杂。
代码度量:
其中:
LOC (Lines Of Code – at method and class granularity)
代码行数,可以看到你的方法和类写了多少行。
CC (Cyclomatic Complexity – Method)
圈复杂度,用于衡量一个模块判定结构的复杂程度,圈复杂度越大说明程序代码质量低,且难以测试和维护。
PC (Parameter Count – Method)
方法中传入的参数个数。
这三次作业的代码量肉眼可见的增加,圈复杂度也从第一次的集中在compareTo到后面floyd等方法,总体来说方法行数平均,没有出现爆方法行数的,圈复杂度除了floyd等少数偏高之外都挺低的,总体来说可以接受。
Bug分析:这三次作业总的来说没有什么BUG,唯一的一个是我在containEdge时忘记先判断点有没有存在图中了,导致数组可能会出现一点点问题,确实时疏忽了。而找BUG我则是写了一个数据生成器,用Random函数随机了很多条指令,然后再去测试他们的程序,然后也用程序生成了一些对时间复杂度要求高的数据来hack别人。
六、学习总结
本次JML单元让我大开眼界,第一次知道还有契约式编程这种东西,JML规格确实可以让程序员了解需求,然后写出符合规范的代码,只要不脱离规格,程序员就可以自由发挥,爱用什么用什么,在团队协作中确实是一个很不错的工具,但是实际运用中可能还是会受限制,首先 撰写JML语言就是一件很复杂的事,有撰写JML的时间早就把代码写完了,其次openJML不够完善,很多符合规范的代码不被识别,最后JML的社区支持太少,很多问题无法第一时间解决。Junit也是新的工具,以前我们想测试一个方法,要手写一个Main方法,然后把这个方法放进去测试,有了Junit一键生成测试方法,妈妈再也不用担心我的测试了~
不过我还是有很大的不足,本次作业可以使用继承与封装使得代码简洁易懂,而我却担心会影响我程序的直观性,怕用了会出现BUG,而没有使用这些面向对象的特性,这是我以后要改正的。
总的来说这些工具确实能帮助我们写代码,减少代码BUG 。这三次作业靠着JML和Junit我们完成了如此复杂的一个图论工程,通过这三次作业,我复习了数据结构,更学会了使用新的工具——JML、Junit。
JML契约式设计——第三单元学习小结的更多相关文章
- 契约式设计(DbC)感想(一)
契约式设计可以理解为正则编程的一种实践: 如果用我的三脚猫能力将这种实践方法形式化的话,大致如下(如有不正确处,请不吝指正): 1.对于方法Method的precondition & post ...
- 重构25-Introduce Design By Contract checks(契约式设计)
契约式设计(DBC,Design By Contract)定义了方法应该包含输入和输出验证.因此,可以确保所有的工作都是基于可用的数据,并且所有的行为都是可预料的.否则,将返回异常或错误并在方法中进行 ...
- 契约式设计(DbC)感想(二)
契约式设计6大原则的理解 在<Design by Contract原则与实践>中,作者定义了契约式设计的6大原则: 区分命令和查询: 将基本查询和派生查询区分开: 针对每个派生查询,设定一 ...
- 第三单元总结——JML契约式编程
OO第三单元博客作业--JML与契约式编程 OO第三单元的三次作业都是在课程组的JML规格下完成.完成作业的过程是契约式编程的过程:设计者完成规格设计,实现者按照规格具体实现.作业正确性的检查同样围绕 ...
- oo第三单元学习总结
OO第三单元小结 一.JML语言理论基础及工具链梳理 在本单元我们学习了JML语言的一些基础知识,能够让我们看懂简单的JML规格并写出对应代码, 主要用到的知识点有: 1.requires 该子句 ...
- Linux内核设计第三周学习总结 跟踪分析Linux内核的启动过程
陈巧然 原创作品 转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 实验步骤 登陆实验楼虚 ...
- 契约式设计 契约式编程 Design by contract
Design by contract - Wikipedia https://en.wikipedia.org/wiki/Design_by_contract What is the use of & ...
- OO第三单元作业小结
一.JML理论基础及应用工具链情况 理论基础 1.JML表达式 \result:表示方法执行后的返回值. \old(expr):表示一个表达式expr在相应方法执行前的取值. \foall:全称量词修 ...
- 《Linux内核分析》第三周学习小结 构造一个简单的Linux系统OS
郝智宇 无转载 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第三周 构造一个简单的Linux系统Me ...
随机推荐
- 《Effective C++》学习笔记(一)
1.const成员函数不能更改成员变量 #include <bits/stdc++.h> using namespace std; class CtextBlock { public: s ...
- u盘安装ubuntu 12.04 server问题解决
问题: 使用UltraISO 9.5.3制作U盘启动盘,ISO文件使用ubuntu-12.04.2-server-i386.iso,ISO文件经过MD5验证是正确的. 将U盘查到计算机上,进bios选 ...
- 【LeetCode】018 4Sum
题目: Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d = ...
- jcrop的bug
1 360(7.1.1.620,内核:31.0.1650.63)的极速模式下,出现裁剪框后,鼠标点击,页面就会滑动到底部. 查看了下源码,发现是下面的代码: function watchKeys() ...
- javascript:function 函数声明和函数表达式 详解
函数声明(缩写为FD)是这样一种函数: 有一个特定的名称 在源码中的位置:要么处于程序级(Program level),要么处于其它函数的主体(FunctionBody)中 在进入上下文阶段创建 影响 ...
- Maven: 自动远程部署
1. 在settings.xml中的Servers节点中增加Server的登录信息: <server> <id>deploy_server_65</id> < ...
- Myeclipse如何使用Maven添加jar包
很多新手都不知道如何在maven项目里添加jar包. 以前我还没接触maven的时候下载过一个demo,是maven项目. 我居然是照着他的pom.xml文件一个一个的写!!! 很多人认为理所当然的东 ...
- MyBatis总结(1)
MyBatis前身是ibatis,是一个数据持久层框架.封装优化了普通JDBC过程, 如数据库连接的创建.设置SQL语句参数.执行SQL语句.事务.结果映射以及资源释放等. MyBatis是一个支持普 ...
- C#使用NPOI读取电子表格Excel到DataGridView中
上篇博文中已经介绍了如何写入Excel文件.这篇再介绍一下 如何从Excel中读取数据并保存到DataGridView中. 从Excel中读取数据并保存至DataGridView中,Excel文件第一 ...
- JS 数组的一些方法
1.push() //可以接受任意参数,然后添加到数组的末尾 2.pop()//栈方法,在数组末尾删除一条数据,并且返回这条数据 3.shift()//队列方法,与pop()相似,但是与其相反,在数组 ...