OO第三单元总结
(一)梳理JML语言的理论基础、应用工具链情况
(1)理论基础
面向对象编程思想中有一个重要原则就是尽量推迟过程性的思考,所以我们在设计面向对象程序时首先要考虑的要干什么而不是怎么干什么。JML作为一种行为接口的规范语言,可以用来指定Java模块的行为,其最基本的用途是作为Java的合同设计(DBC)语言。
使用 JML 来说明性地描述所希望的类和方法的行为,可以显著地改善整个开发过程。将建模表示法添加到 Java 代码中,其好处包括以下几点:
-
- 能更加精确地描述代码所完成的任务
- 能有效地发现和纠正错误
- 能减少随着应用程序的进展而引入错误的机会
- 能较早地发现客户没有正确使用类
- 能产生始终与应用程序代码保持同步的精确文档
近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。一般而言,JML有两种主要的用法:
1) 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
2) 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
JML表达式实现描述:
原子表达式:
\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表达式:返回指定变量中满足相应条件的取值个数。
(2)应用工具链
首先通过开源的JML编译器,比如OpenJml,编译含有JML标记的代码。Openjml中还包含z3等SMT Solver,可以对代码等价性进行验证。通过Openjml -esc 选项对代码的静态验证是不依赖JML的,SMT Solver会自动整理JML。
JML UnitNG可以生成一个Java类文件测试的框架,基于JML并结合Openjml的-rac运行时检查选项,实现对代码的自动化测试。
JMLdoc工具与Javadoc工具类似,可在生成的HTML格式文档中包含JML规范。
(二)JMLUnitNG/JMLUnit(感激伦佬...
// demo/Lalala.java package demo; public class Lalala { /*@ public normal_behaviour @ ensures \result == lhs * rhs; */ public static int compare(int lhs, int rhs) { return lhs * rhs; } public static void main(String[] args) { compare(5201314,4131025); } }
分别执行
java -jar jmlunitng.jar demo/Lalala.java javac -cp jmlunitng.jar demo/*.java javac -cp jmlunitng.jar demo/Lalala_JML_Data/*.java java -jar openjml.jar -rac demo/Lalala.java java -cp jmlunitng.jar demo.Lalala_JML_Test
得到结果如图:

个人认为这种边界性测试对功能性的小方法的价值确实很大。
(三)架构设计
第一次作业:

第一次作业主要的目的是熟悉jml语言。
对于MyPath类,直接按规格要求完成即可。
对于MyPathContainer类,由于查询指令相对耗时,因而应尽量在执行其他命令时通过记录某些信息完成查询的目的。
- 使用HashMap<Integer, Path> 实现根据id查找路径。
- 使用HashMap<Path, Integer> 实现根据路径查找id。
- 使用HashMap<Integer, Integer> 记录当前容器内所有路径中不同结点的出现次数。每次增加路径时,该路径中各结点对应的次数加一;每次删除路径的时候,将该路径中的每个结点对应的值减1,若其值为0则从HashMap中 删去该结点。每次查询只需返回 key 对应的次数。
第二次作业:

本次作业中MyPath类可以基本沿用上次代码,MyGraph类可以继承MyPathContainer类或是复制粘贴原有方法。
对于本次要求实现的isConnected和ShortestPathLength方法,可以通过Floyd算法一并实现。相关数据结构有:
- 用于映射结点号的HashMap<Integer,Integer>
- 125*125的邻接矩阵(静态数组),其[i,j]位置上的值为结点i和结点j之间边的个数。
- 125*125的shortestpathlength数组(静态数组),其[I,j]位置上的值为结点i和结点j之间的最短路径长度。
当出现增删路径操作时,使用floyd算法刷新shortestpathlength数组;调用isconnected方法时,若shortestpathlength数组对应位置上值并非Infinity则返回1,否则返回0。同理,查询最短路径长度时,返回路径长度或抛出“不连通”异常。
第三次作业:

本次作业在上次作业的基础上加入了转乘的概念,需要实现计算票价、转乘次数、不满意度的方法。
其中,MyPath类、MyGraph类无需更改,MyRailwaySystem类继承MyGraph类,再按规格完成其他方法即可。
计算票价、转乘次数、不满意度时,可复用MyPathContainer中floyd计算方法,变更权重即可。
(四)按照作业分析代码实现的bug和修复情况
第一次作业时在删除路径时,颠倒了删路径和更新结点信息(更新各结点的总出现次数)的顺序——先删路径再删节点信息时可能出现:删除路径后某结点为孤立点(该结点映射关系会被去除),再刷新结点出现次数时出现不存在映射关系的情况。因此在挂掉了很多点。修复时只是调整了两个方法的调用顺序而已。
第二次作业由于将只存在自环的点判定为了孤立点,导致出现了一些小问题。
第三次未被测出bug。
(五)阐述对规格撰写和理解上的心得体会
本单元的三次作业重在体会规格的价值、理解JML、实现代码功能,个人感觉在拿到规格后确实对需求有了更好的把握,对边界情况有了更清晰的界定,因此代码实现起来更加轻松而愉快,唯一要考虑的也只是实现方式的复杂度而已。同时,由于限定好了接口和逻辑层次,也避免了逻辑混乱、推倒重来的风险,并在一定程度上优化了代码风格。
只不过,编写规格的能力还需要在不断的练习中逐步提高,同时,”规格化“、”层次化“的理念,也需要在不断的磨练中加以融会贯通,这是仍需在未来不断努力的。
浙公网安备 33010602011771号