OO第三单元总结
(一)JML语言
1)理论基础
注释结构
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式 为 //@annotation ,块注释的方式为 /* @ annotation @*/ 。
JML表达式
JML的表达式是对Java表达式的扩展,新增了一些操作符和原子表达式。同样JML表达式中的操作符也有优先级的概念。
1)表达式:
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式:返回给定范围内的表达式的和。
2)操作符:
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。此外,JML专门又定义 了如下四类操作符。
(1) 子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。
(2) 等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达 式,这两个表达式的意思是 b_expr1==b_expr2 或者 b_expr1!=b_expr2 。可以看出,这两个操作符和Java中的 == 和 != 具有相同的效果,按照JML语言定义, <==> 比 == 的优先级要低,同样 <=!=> 比 != 的优先级低。
(3) 推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1 。对于表达式 b_expr1==>b_expr2 而言,当 b_expr1==false ,或者 b_expr1==true 且 b_expr2==true 时,整个表达式的值为 true 。
(4) 变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来 引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变 量。变量引用操作符经常在assignable句子中使用,如 assignable \nothing 表示当前作用域下每个变量都不可以 在方法执行过程中被赋值。
方法规格
1)前置条件
前置条件通过requires子句来表示: requires P;。其中requires是JML关键词,表达的意思是“要求调用者确保P为 真”。注意,方法规格中可以有多个requires子句,是并列关系。如果想要表达或的逻辑,则应该使用一个requires子句,在其中的谓词P中使用逻辑或操作符来表示相应的约束场 景: requires P1||P2;。
2)后置条件
后置条件通过ensures子句来表示: ensures P;。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。同样,方法规格中可以有多个ensures子句,是并列关系。如果想要表达或的逻辑,这应该在在一个ensures子句 中使用逻辑或( || )操作符来表示相应的约束场景: ensures P1||P2;。
3)副作用范围限定
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法 规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词 assignable 或者 modifiable 。
4)正常与异常行为
normal_behavior表示正常情况下的jml,exceptional_behavior表示异常情况下jml,两者之间用also连接。signals子句用来抛出异常。
类规格
1)不变式invariant
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中 invariant 为关 键词, P为谓词。
2)状态变化约束constraint
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定 invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的 关系进行约束。
2)应用工具链
JMLUnitNG
可以根据JML生成一个java类文件测试的框架,结合openjml的-rac运行时检查选项,实现对代码的自动化测试。
OPENJML
在Intellij中安装openJML插件,可以进行规格检查。
(二)部署JMLUnitNG/JMLUnit,生成测试用例
使用JMLUnit,针对实现的Railway接口生成测试用例。
测试样例完整代码如下:
1 package path; 2 3 import org.junit.After; 4 import org.junit.Assert; 5 import org.junit.Before; 6 import org.junit.Test; 7 8 public class MyRailwayTest { 9 10 private MyRailway myrailway = new MyRailway();11 private MyPath path1;12 private MyPath path2;13 private MyPath path3;14 15 @Before16 public void setUp() {17 int[] nodeList1 = {1,2,3,4};18 int[] nodeList2 = {1,2,3,4,5};19 int[] nodeList3 = {1,2,2,3,4};20 path1 = new MyPath(nodeList1);21 path2 = new MyPath(nodeList2);22 path3 = new MyPath(nodeList3);23 System.out.println("Before Method");24 }25 26 @After27 public void tearDown() {28 System.out.println("After Method");29 }30 31 @Test32 public void addPath() {33 myrailway.addPath(path1);34 Assert.assertEquals(1, myrailway.size());35 Assert.assertTrue(myrailway.containsPathId(1));36 Assert.assertTrue(myrailway.containsPath(path1));37 myrailway.addPath(path1);38 Assert.assertEquals(1, myrailway.size());39 myrailway.addPath(path2);40 Assert.assertEquals(2, myrailway.size());41 Assert.assertTrue(myrailway.containsPathId(2));42 Assert.assertTrue(myrailway.containsPath(path2));43 myrailway.addPath(path3);44 Assert.assertEquals(3, myrailway.size());45 Assert.assertTrue(myrailway.containsPathId(3));46 Assert.assertTrue(myrailway.containsPath(path3));47 }48 49 @Test50 public void size() {51 myrailway.addPath(path1);52 Assert.assertEquals(1, myrailway.size());53 myrailway.addPath(path1);54 Assert.assertEquals(1, myrailway.size());55 myrailway.addPath(path2);56 Assert.assertTrue(myrailway.containsPath(path2));57 myrailway.addPath(path3);58 Assert.assertEquals(3, myrailway.size());59 }60 }
运行结果如下:
上述生成的测试样例主要是对路径的存储、删除、查找以及路径数量等功能进行测试。
(三)架构设计
1)第一次作业
第一次作业主要包含三个类:
MPath:继承Path类接口,使用一维数组存储路径上各个节点id
MyPaCon:继承PathContainer类接口,使用两个ArrayList分别来存储各条路径及其路径id
MainClass:主类
类图如下:
度量分析如下:
2)第二次作业
第二次作业主要包含三个类:
MyPath:继承Path类接口,使用一维数组存储路径上各个节点id,与之前相比没有变化
MyGraph:继承Graph类接口,在第一次作业基础上,通过实现广度优先算法来寻找最短路径。此外,使用一个ArrayList来存储此时找到的最短路径的节点序列。
MainClass:主类
类图如下:
度量分析如下:
3)第三次作业
本次作业在上次作业基础上将图的存储以及相关操作封装成类,此外新建了新的节点类,用来存储图中的节点信息。本次作业的主要使用的算法为Dijkstra算法,最短路径、最少票价以及最少换乘等问题均可以该算法实现,区别在于路径权重的计算以及对于在多条路径出现的节点的处理(是否将其分为多个节点)。对于连通块问题,使用着色算法即可解决。
本次作业包含类较多,此处仅列举几个较为重要的类进行说明:
Graph:图的封装类,主要是完成构图、删边、插边以及相关查询操作等。
Node:图节点的封装类,主要是存储图中每个节点的信息:不满意度、id以及所在路径id等。
MyRailway:继承Railway类接口。
类图如下:
度量分析如下:
(四)Bug以及修复情况
三次作业都基本正确实现了JML规格,基本完成了要求的功能,但三次作业强侧中都出现了或多或少的时间过长的错误。从之前的度量分析中可以看出,三次作业实现的复杂度较高。
提高效率的改进:
<1>使用效率较高的Hashmap,放弃使用效率较低的ArrayList
<2>使用缓存机制,例如将各个节点到其它各点的最短距离、最短票价、最短换乘等信息存储起来。当需要相关信息时,直接到缓存中读取即可,只有插入新的边或删去边时,才执行Dijkstra算法对缓存进行更新。
<3>通过增加一些条件的判断,省去一些不必要的时间开销
(五)心得体会
好的代码规格一方面可以增加了代码的易读性,也便于了代码的维护,从而使得多人合作的效率更高,避免了工作上的冲突;另一方面可以对代码的逻辑、风格进行一定的规范,也对于代码实现提供了一定的思路。
在撰写规格时,需要大量的思考,需要考虑到各种情况的发生,因为规格是一个代码的根基,规格的错误可能导致之前的所有工作都成为徒劳。
此外,不要将代码实现完全依赖于规格,尤其是数据结构的选择上,规格仅是保证了正确性,而代码的实现除了正确性外还有考虑的效率问题。