本博文是2019年北航面向对象(OO)课程第三单元作业(规格化设计)的总结。三次作业的要求大致如下:

  • 第一次作业:实现一个路径管理系统,可以通过各类输入指令来进行数据的增删查改等交互。
  • 第二次作业:实现一个无向图系统,可以进行基于无向图的一些查询操作。
  • 第三次作业:实现一个简单地铁系统,可以进行一些基本的查询操作

源代码及项目要求均已发布到 github ,读者可以下载检查。以下将对这一单元作业进行简单总结。

JML规格化设计

Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为。它结合了契约方法设计Larch系列接口规范语言的基于模型的规范方法,以及细化演算的一些理论。

理论基础

JML是一种行为界面规范语言(BISL)。在这种被称为面向模型的编程风格中,我们需要指定方法或抽象数据类型的接口及其行为。

方法接口是从程序的其他部分使用它所需的信息。对于JML,这是调用方法或使用属性或类所需的Java语法和类型信息。方法接口包括诸如方法的名称、修饰符、参数数量、返回类型、可能抛出的异常等等;属性接口包括名称和类型及其修饰符;类接口包括名称、修饰符、包、超类,以及它声明和继承的属性和方法的接口。JML使用Java的语法指定所有这些接口信息。

方法行为描述了一组能够执行的状态转换:定义调用方法的一组状态,允许方法可以赋值的一组属性,以及调用状态和返回状态之间的关系。方法的状态由逻辑断言描述,称为方法的前置条件。这些状态与正常返回可能导致的状态之间允许的关系由另一个称为方法正常后置条件的逻辑断言描述。类似地,这些前置状态与抛出异常可能导致的状态之间的关系由方法的异常后置条件描述。

工具链

本单元中我们使用到的工具主要有OpenJMLJMLUnitNG

  • OpenJML 可以对JML注释进行静态或动态检查,验证程序是否符合规格
  • JMLUnitNG 可以自动生成测试样例

规格验证

验证代码

由于项目代码过于庞大,无法使用工具对其进行验证。因此我将代码的部分内容抽离,在新的子项目中进行测试。子项目的文件树如下

  1. D:.
  2. jmlunitng.jar

  3. ├─model
  4. Path.java
  5. PathContainer.java

  6. └─unit

其中,unit是生成的.class文件路径。java源文件的内容如下

  1. // .\model\Path.java
  2. package model;
  3. public class Path {
  4. final private /*@ spec_public @*/int[] nodes;
  5. public Path() {
  6. this(1, 2, 3, 4, 5);
  7. }
  8. public Path(int... nodes) {
  9. this.nodes = nodes;
  10. }
  11. public int[] getNodes() {
  12. return nodes;
  13. }
  14. public int size() {
  15. return nodes.length;
  16. }
  17. @Override
  18. public String toString() {
  19. String result = "[";
  20. for (int i = 0; i < size(); i++) {
  21. result += nodes[i];
  22. result += ", ";
  23. }
  24. result = result.substring(0, result.length() - 2) + "]";
  25. return result;
  26. }
  27. }
  1. // .\model\PathContainer.java
  2. package model;
  3. public class PathContainer {
  4. private /*@ spec_public @*/ Path[] pList;
  5. private /*@ spec_public @*/ int[] pidList;
  6. private int size = 0;
  7. public PathContainer() {
  8. pList = new Path[10];
  9. pidList = new int[10];
  10. addPath(new Path(1, 2, 3));
  11. addPath(new Path());
  12. addPath(new Path(4, 1, 2, 3));
  13. }
  14. public /*@pure@*/ int size() {
  15. return size;
  16. }
  17. /*@
  18. @ ensures path == null ==> \result == 0;
  19. @ ensures path != null ==> \result == size();
  20. @*/
  21. public int addPath(Path path) {
  22. pList[size] = path;
  23. pidList[size] = size + 1;
  24. size++;
  25. return size;
  26. }
  27. /*@
  28. @ ensures \result == pathId <= size() && pathId >= 1;
  29. @*/
  30. public boolean containsPathId(int pathId) {
  31. for (int i = 0; i < size; i++) {
  32. if (pidList[i] == pathId) {
  33. return true;
  34. }
  35. }
  36. return false;
  37. }
  38. }

代码静态检查

有关openjml批处理脚本的内容可以查看OpenJML入门,这里直接使用命令进行代码静态检查。

  1. D:\>openjml -esc -prove model\*.java
  2. model\Path.java:26: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method toString
  3. result += nodes[i];
  4. ^
  5. model\Path.java:26: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method toString
  6. result += nodes[i];
  7. ^
  8. model\Path.java:29: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: C:\Users\ThinkPad\Documents\Java\openjml-0.8.42-20190401\openjml.jar(specs/java/lang/Object.jml):194: 注: ) in method toString
  9. result = result.substring(0, result.length() - 2) + "]";
  10. ^
  11. C:\Users\ThinkPad\Documents\Java\openjml-0.8.42-20190401\openjml.jar(specs/java/lang/Object.jml):194: 警告: Associated declaration: model\Path.java:29: 注:
  12. /*-RAC@ public normal_behavior // FIXME - do we want this to be normal_behavior?
  13. ^
  14. model\PathContainer.java:24: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method addPath
  15. pList[size] = path;
  16. ^
  17. model\PathContainer.java:24: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method addPath
  18. pList[size] = path;
  19. ^
  20. model\PathContainer.java:24: 警告: The prover cannot establish an assertion (PossiblyBadArrayAssignment) in method addPath
  21. pList[size] = path;
  22. ^
  23. model\PathContainer.java:25: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method addPath
  24. pidList[size] = size + 1;
  25. ^
  26. model\PathContainer.java:39: 警告: The prover cannot establish an assertion (Postcondition: model\PathContainer.java:31: 注: ) in method containsPathId
  27. return false;
  28. ^
  29. model\PathContainer.java:31: 警告: Associated declaration: model\PathContainer.java:39: 注:
  30. @ ensures \result == (pathId <= size() && pathId >= 1);
  31. ^
  32. model\PathContainer.java:35: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method containsPathId
  33. if (pidList[i] == pathId) {
  34. ^
  35. model\PathContainer.java:35: 警告: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method containsPathId
  36. if (pidList[i] == pathId) {
  37. ^
  38. 12 个警告

可以看到,静态检查的警告主要是针对数组下标越界这一问题。但查看源码发现这个问题实际上并不会发生,因为for循环中已经限制了下标大小,因此输出信息也只是警告而已。下面我们来更改一下源码,将PathContainer的第 \(22\) 行改为

  1. @ ensures path == null => \result == 0;

这时再次运行会发现

  1. model\PathContainer.java:21: 错误: 非法的表达式开始
  2. @ ensures path == null => \result == 0;
  3. ^
  4. model\PathContainer.java:21: 错误: 意外的类型
  5. @ ensures path == null => \result == 0;
  6. ^
  7. 需要: 变量
  8. 找到:
  9. model\PathContainer.java:21: 错误: Assignments are not allowed where pure expressions are expected
  10. @ ensures path == null => \result == 0;
  11. ^
  12. 3 个错误

原本的警告变成了错误,说明静态检查检查出了错误。事实上,静态检查不仅可以检查语法,还可以检查算术溢出等,这里不再赘述。

自动生成测试样例

生成结果

生成测试样例的脚本如下

  1. ::jmlunitng.bat
  2. java -jar jmlunitng.jar -d unit model\*.java
  3. copy model\*.java unit\model\
  4. javac -cp jmlunitng.jar;unit\; unit\model\*.java
  5. call openjml -rac -d unit model\*.java
  6. java -cp jmlunitng.jar;unit\; model.Path_JML_Test
  7. java -cp jmlunitng.jar;unit\; model.PathContainer_JML_Test
  8. cmd.exe

输出如下

  1. [TestNG] Running:
  2. Command line suite
  3. Passed: racEnabled()
  4. Passed: constructor Path()
  5. Failed: constructor Path(null)
  6. Passed: <<[1, 2, 3, 4, 5]>>.getNodes()
  7. Passed: <<[1, 2, 3, 4, 5]>>.size()
  8. Passed: <<[1, 2, 3, 4, 5]>>.toString()
  9. ===============================================
  10. Command line suite
  11. Total tests run: 6, Failures: 1, Skips: 0
  12. ===============================================
  13. [TestNG] Running:
  14. Command line suite
  15. Passed: racEnabled()
  16. Passed: constructor PathContainer()
  17. Passed: <<model.PathContainer@68fb2c38>>.addPath(null)
  18. Passed: <<model.PathContainer@567d299b>>.addPath([1, 2, 3, 4, 5])
  19. Failed: <<model.PathContainer@6c629d6e>>.containsPathId(-2147483648)
  20. Failed: <<model.PathContainer@5ecddf8f>>.containsPathId(0)
  21. Passed: <<model.PathContainer@3f102e87>>.containsPathId(2147483647)
  22. Passed: <<model.PathContainer@27abe2cd>>.size()
  23. ===============================================
  24. Command line suite
  25. Total tests run: 8, Failures: 2, Skips: 0
  26. ===============================================

可以看出,JMLUnitNG检查的方式是将边界数据带入,而且不管方法有没有规格描述。

错误分析

空指针异常

对于引用类型参数,工具会自动传入null以测试鲁棒性。在Path::new中,我没有考虑到传入参数为null的情况。这时如果调用size方法就会引发空指针异常。

JML描述错误

下方PathContainer的两个错误是由于JML规格写错造成的。我以为以下两条语句是等价的,实则不然。

  1. /*@ ensures \result == pathId <= size && pathId >= 1; @*/
  2. /*@ ensures \result == (pathId <= size && pathId >= 1); @*/

将规格描述修改后,自动生成测试样例的检测也就通过了。

作业设计

第九次作业

架构

核心架构中,只涉及两个接口和其实现类,符合面向对象的里氏替换原则。

代码实现

由于是本单元的第一次作业,代码实现也比较简单,严格按照规格编程即可。主要难点在于distinct_node_count方法的复杂度如何降低,这里我使用了函数式编程。因此本次作业中没有出现Bug。

第十次作业

架构

可以看出,第二次作业比第一次复杂许多。这主要是因为增加了一组接口Graph以及自定义数据结构datastructure.Graph

本次作业的架构是上次作业的直接拓展,并没有更改上次作业的逻辑。因为Graph接口本身继承自PathContainer接口,所以实现类MyGraph也继承自MyPathContainer,以提高代码复用性。此外,为了将业务逻辑和底层实现分离,我将图结构抽象出来作为数据结构类,为MyGraph提供支持。这样一来,MyGraph的工作量就大大降低了,便于维护和测试。

代码实现

由于MyGraph继承了MyPathContainer,因此很多查询指令不需要重复实现。但对于图变更指令,由于其会影响到MyGraph类中的属性,因此我对其进行了重载。下述的两个Bug都是因为重载时没有考虑到所有情况而产生的。

addPath

最初,我的addPath方法如下

  1. @Override
  2. public int addPath(Path path) {
  3. int result = super.addPath(path);
  4. if (result == 0) {
  5. return result;
  6. }
  7. // operations
  8. return result;
  9. }

在这个函数中我只考虑到了path不合法的情况,却没有考虑到如果path事先存在,则super.addPath不会执行这个问题。而operations部分的代码,只要看到path合法就一定会执行,因此造成了superthis不匹配的问题。

为了解决这个问题,我在函数开头加入了判断

  1. @Override
  2. public int addPath(Path path) {
  3. try {
  4. return super.getPathId(path);
  5. } catch (PathNotFoundException e) { /* nothing */ }
  6. int result = super.addPath(path);
  7. // ...
  8. return result;
  9. }

这样就可以排除path事先存在这一影响因素了。

removePath

在进行了对于MyPathContainer的删除操作后,MyGraph需要对其内部元素graph同样执行删除操作。一开始我的代码如下

  1. int firstNode = 0;
  2. int secondNode = path.getNode(0);
  3. for (int i = 1; i < path.size(); i++) {
  4. firstNode = secondNode;
  5. secondNode = path.getNode(i);
  6. graph.removeEdge(firstNode, secondNode);
  7. graph.removeIfIsolated(firstNode);
  8. }
  9. graph.removeIfIsolated(secondNode);

乍看上去,似乎没什么问题。我们遍历了路径上的每条边和每个顶点,依次将其删除。然而问题出现在最后一行。

如果一条路径的最后两个顶点相同,而且是这个图中唯一的一对顶点。那么在循环时,首先会经过这个点一次。这时,由于最后一条边已经被删除了,所以这个顶点是孤立点,也被删除。这时跳出循环的话,会在执行一次删除操作,引发空指针异常。为了解决这个问题,我特判了这种情况

  1. if (firstNode != secondNode) { graph.removeIfIsolated(secondNode); }

第十一次作业

架构

本次作业中我对代码进行了重构。按照接口的继承关系,我本来应该让实现类MyRailwaySystem继承MyGraph的。但是由于本次作业的数据基本限制和上次作业相比有很大变化,因此我重构了datastructure.Graph类,使其能处理泛型数据。这样一来,原本的MyGraph类也需要被重构。但是因为MyGraph本就不需要在代码中出现,因此我将其功能全部转移到了MyRailwaySystem中进行了重构。

代码实现

本次作业中没有出现Bug。原本担心的TLE也并没有出现,可能是因为静态数组的效率之高弥补了没有cache的缺陷。

心得体会

很早就听说程序的正确性可以用形式化证明来保证,但是以前一直没有使用过相关工具。总的来说本单元作业让我体会到了规格化设计的神奇之处。OpenJML这样的工具虽然还不完善,但已经可以帮我们检查一些隐蔽的错误了。而在规格描述方面,这样的语言也提供了标准的描述方法,避免了自然语言的二义性。可以说是很长见识了。

但是在本单元我认为还存在一些问题。首先,我原先以为本单元作业是有关设计模式的,没想到是用规格描述来代替指导书……其次,规格设计应该是抽象的,提纲挈领的描述函数调用接口,这样才能方便人们进行验证。但事实是规格往往比代码的内容要多,这一方面造成了规格设计和程序设计双方交流所需的时间,另一方面完全没有给形式化验证带来任何便利。因为工具并不能完全自动的验证程序正确性,在关键地方还是需要人来进行逻辑推理。这时如果人面对着比代码还要多的规格描述,我觉得人可能会选择直接检查代码。

因此我认为,本单元作业确实教会了我关于规格化设计的基础知识,但我在未来的工作中不会选择规格设计。

参考

Java Modelling Language

BUAA OO 2019 第三单元作业总结的更多相关文章

  1. BUAA OO 2019 第四单元作业总结

    目录 第四单元总结 总 UML UML 类图 UML 时序图 UML 状态图 架构设计 第十三次作业 第十四次作业 课程总结 历次作业总结 架构设计 面向对象方法理解 测试方法理解与实践 改进建议 尽 ...

  2. BUAA OO 2019 第一单元作业总结

    目录 总 架构 Controller​ Model​ 输入处理 代码静态分析 行数 方法复杂度 UML​ 类图 优点 缺点 坑 输入 非法的空白字符 输入的简并处理 运算 浅拷贝 可变类型与不可变类型 ...

  3. BUAA OO 2019 第二单元作业总结

    目录 总 架构 controller model view 优化算法 Look 算法 多种算法取优 预测未来 多线程 第五次作业 第六次作业 第七次作业 代码静态分析 UML 类图 类复杂度 类总代码 ...

  4. OO第三单元作业总结

    OO第三单元作业总结--JML 第三单元的主题是JML规格的学习,其中的三次作业也是围绕JML规格的实现所展开的(虽然感觉作业中最难的还是如何正确适用数据结构以及如何正确地对于时间复杂度进行优化). ...

  5. 【OO学习】OO第三单元作业总结

    [OO学习]OO第三单元作业总结 第三单元,我们学习了JML语言,用来进行形式化设计.本单元包括三次作业,通过给定的JML来实行了一个对路径的管理系统,最后完成了一个地铁系统,来管理不同的线路,求得关 ...

  6. OO第三单元作业——魔教规格

    OO第三单元作业--魔教规格 JML的理论基础和相关工具   JML(Java Modeling Language,Java建模语言),在Java代码种增加了一些符号,这些符号用来标志一个方法是干什么 ...

  7. OO第三单元作业(JML)总结

    OO第三单元作业(JML)总结 目录 OO第三单元作业(JML)总结 JML语言知识梳理 使用jml的目的 jml注释结构 jml表达式 方法规格 类型规格 SMT Solver 部署JMLUnitN ...

  8. 2019北航OO第三单元作业总结

    1.梳理JML语言的理论基础.应用工具链情况 JML基础理论: JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言.JML是一种行为接口规格语言,基 ...

  9. OO第三单元作业小结

    一.JML理论基础及应用工具链情况 理论基础 1.JML表达式 \result:表示方法执行后的返回值. \old(expr):表示一个表达式expr在相应方法执行前的取值. \foall:全称量词修 ...

随机推荐

  1. D3.js的v5版本入门教程(第三章)—— 选择元素和绑定数据

    D3.js的v5版本入门教程(第三章) 在D3.js中,选择元素和绑定元素是最基本的内容,也是很重要的内容,等你看完整个教程后你会发现,这些D3.js教程都是在选择元素和绑定元素的基础上展开后续工作的 ...

  2. Fiddler导出JMX文件配置

    (1)安装fiddler jmeter(免安装) 注意事项!fiddler版本必须在v4.6.2以上(插件支持的是4.6版本), jmeter版本最好在v3.0以上,版本太低容易导致导出不成功 这里我 ...

  3. Deep-learning augmented RNA-seq analysis of transcript splicing | 用深度学习预测可变剪切

    可变剪切的预测已经很流行了,目前主要有两个流派: 用DNA序列以及variant来预测可变剪切:GeneSplicer.MaxEntScan.dbscSNV.S-CAP.MMSplice.clinVa ...

  4. ggplot常见语法汇总查询

    主图 散点图 柱状图 折线图 小提琴图 点图 进化树 圈图 Alluvial图 Sankey Diagram plot(getSankey(colData(muraro)$cell_type1, mu ...

  5. Python 拼接字符串的几种方式

    在学习Python(3x)的过程中,在拼接字符串的时候遇到了些问题,所以抽点时间整理一下Python 拼接字符串的几种方式. 方式1,使用加号(+)连接,使用加号连接各个变量或者元素必须是字符串类型( ...

  6. arcgis python脚本工具实例教程—栅格范围提取至多边形要素类

    arcgis python脚本工具实例教程-栅格范围提取至多边形要素类 商务合作,科技咨询,版权转让:向日葵,135-4855_4328,xiexiaokui#qq.com 功能:提取栅格数据的范围, ...

  7. Nginx记录-Proxy_pass多个应用配置(转载)

    1. 在http节点下,加入upstream节点. upstream linuxidc {       server 10.0.6.108:7080;       server 10.0.0.85:8 ...

  8. Docker容器(六)——创建docker私有化仓库

    docker私有化仓库是为了节约带宽(外网速度慢或者干脆不能连外网),以及自己定制系统. (1).环境 youxi1 192.168.5.101 docker私有化仓库 youxi2 192.168. ...

  9. PS弧形边缘的去黑色背景色

    按照理论来说,纯色的字体加上纯色的背景,然后保存成png文件,然后用色彩范围选择纯色的背景,去掉背景,这样应该能得到原来设置的纯色的字体,但实际测试后不是这样的.如果是矩形等,是纯色,但是Photos ...

  10. Python Tkinter 窗口创建与布局

    做界面,首先需要创建一个窗口,Python Tkinter创建窗口很简单:(注意,Tkinter的包名因Python的版本不同存在差异,有两种:Tkinter和tkinter,读者若发现程序不能运行, ...