面向对象编程第三次博客作业——JML与架构总结
一、JML理论知识梳理
描述方法预期:将注释添加到java代码中,确定方法所执行的内容,不必说明所执行的过程
JML注释始终位于comment内部,不会对正常编译的代码产生影响
开放源码 JML 编译器:捕捉代码中的错误,保持JML注释与代码保持同步
JML的两种主要的用法:
(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。 (2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
两种规格变量:静态,实例
在Interface中声明规格变量,必须明确变量的类别
静态:
//@public static model non_null int []elements
实例:
//@public instance model non_null int []elements
1.注释结构
两种注释方式:
1.行注释
//@annotation
2.块注释
/* @ annotation @ */
JML注释语句放在被注释成份的邻近上部
2.JML表达式
对于Java表达式的扩展
2.1原子表达式
\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
对应的准确类型。
2.2 量化表达式
\forall
表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists 表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式:返回给定范围内的表达式的和。
\product表达式:返回给定范围内的表达式的连乘结果。
\max表达式:返回给定范围内的表达式的最大值。
\min表达式:返回给定范围内的表达式的最小值。
\num_of
表达式:返回指定变量中满足相应条件的取值个数。
2.3 集合表达式
集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
集合构造表达式的一般形式为:
new ST {T x|R(x)&&P(x)} /*R(x)对应集合中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。*/
2.4 操作符
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。
(1) 子类型关系操作符: E1<:E2
,如果类型E1
是类型E2
的子类型(sub type),则该表达式的结果为真,否则为假。如果E1
和E2
是相同的类型,该表达式的结果也为真,
(2) 等价关系操作符:
b_expr1<==>b_expr2 // b_expr1 == b_expr2 b_expr1<=!=>b_expr2 // b_expr1 != b_expr2
(3) 推理操作符: 对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且 b_expr2==true
时,整个表达式的值为 true 。
b_expr1==>b_expr2
b_expr2<==b_expr1
(4) 变量引用操作符:变量引用操作符经常在assignable句子中使用
\nothing指示一个空集;
\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。
3、方法规格
(1)前置条件 reqquires
(2)后置条件 ensures
(3)副作用范围限定 assignable modifiable
(4)signals子句
4、类规格
(1)状态变化约束 constraint
(2)不变式 invariant
5、应用工具链
OpenJml
工具 :
OpenJML
最基本的功能就是对JML
注释的完整性进行检查。检查包括经典的类型检查、变量可见性与可写性等。通过命令行使用OpenJML
时,可以通过-check
参数(缺省)指定类型检查。
以一个非常简单的代码举例:
public class main { /*@ @ require args != null @*/ public static void main(String[] args) { System.out.print(args); } }
运行报错信息:
C:\>openjml main.java main.java:17: 错误: 需要';' @ require args != null ^ main.java:17: 错误: 找不到符号 @ require args != null ^ 符号: 类 require 位置: 类 main 2 个错误
(我仅仅根据教程进行了简单的尝试,具体使用还需要更多的理解和研究)
二、jmlUnitNg
根据讨论区大佬的方法,进行了尝试,结果如下:
三、架构设计梳理
第一次作业
主要使用的数据结构:Hashmap
这次作业主要考察的点在于如何根据规格写出正确的代码,需要优化的部分主要集中于查找上。为了提高查找的便捷性和速度,采取了Hashmap
来进行数据的储存。将复杂度集中于对于Path List的更新部分,这样在对路径进行查找操作时就十分快捷。
第二次作业
这次作业在第一次作业的基础上构建了图,需要查找的部分也比之前复杂了不少。
关于最短路径的查询,我采用了弗洛伊德算法,在每次对图进行更新的时候同时更新弗洛伊德矩阵,相比而言这是一个简单有效的算法。
主要使用的数据结构还是Hashmap
第三次作业
这次作业在前两次作业的基础上要实现一个地铁系统,需要增加的有最短路径、最低票价、最少换乘、最低不满意度这四种查询,这四种其实都可以使用弗洛伊德算法实现,相对于迪杰斯特拉算法虽然失去了一定的时间效率,但是简单容易完成。
在这次作业前,我首先对第二次作业进行了优化和修改,将佛洛依德算法进行了优化和改变,不再直接对Hashmap
进行迭代遍历,因为这样的速度非常慢。采用了首先对节点进行线性映射,然后构造二维数组,这样提高了在遍历时的效率,毕竟弗洛伊德算法的时间复杂度高达O(n^3)
。
四、bug及修复
第一次作业:
这次的bug非常的弱智,在对路径进行偏序比较的时候少写了两个break
,这种错误在学习C语言的时候就不应该再出现了,没想到现在还会有orz
第二次作业:
对Hashmap
直接进行三重的循环遍历,没有考虑这个数据结构在迭代时对于时间的高额花费,从而导致了强测的CPU时间崩盘
第三次作业:
对于三种查询算法理解不够,导致正确性很低。
五、对规格撰写的理解和体会
规格化设计的思想可以避免很多bug的出现,这种设计模式在最开始就设计好了每个方法、每个对象的执行逻辑,理论上来说,只要规格设计没有问题,严格按照规格来编写的代码就是正确的;同时,规格化设计对于代码的全面测试等都是非常方便的。