OO第三单元作业总结
梳理JML语言的理论基础、应用工具链情况
JML全称Java modeling language是一种行为接口规格语言(Behavior Interface Specification Language)。实际上它的作用就是对类和类的方法属性以javadoc注释的形式进行形式化的表示。通过JML所提供的描述行为的结构,例如全称谓词\forall,存在谓词\exists等,来定义java类中方法属性的规范动作。下面首先介绍一下JML语言的理论基础(主要是实验中有使用过的语法结构)
JML语言理论基础
JML以javadoc注释的方式来表示规格,每行都以@为开始。并类似于正常注释语法,可以行注释//和块注释/.../。
表达式部分
原子表达式
\result表达式表示一个非void类型的方法执行所获得的结果,即方法return的结果。
\old(...)表达式,用来表示一个表达式在相应方法执行前的取值。该表达式可以用来评估对象是否发生变化,以及发生变化是否符合预期。
\not_assigned(x,y,...);\not_modified(x,y,...);\nonullelements()等也是原子表达式,本单元实验中并没有具体涉及,所以在此不再一一赘述。
量化表达式
\forall表达式 是全称量词修饰的表达式,表示对于给定范围的元素,每个元素都满足相应的约束。
\exists表达式 是存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束
\sum表达式 返回给定范围内的表达式的和
\max表达式 返回给定范围内的表达式的最大值
方法规格中的重要组成部分
前置条件(pre-condition) 前置条件通过requires子句来表示。其中requires是JML的关键词,表达意思是要求调用者必须确保前置条件为真。
后置条件(post-condition) 后置条件通过ensures子句来表示。要求方法的实现者必须确保方法执行返回结果满足后置条件。
类型规格中的重要组成部分
不变式invariant 要求在所有可见状态下都必须满足的特性。
状态变化约束constraint 对象的状态在变化时满足的约束。
JML应用工具链
JML相应的工具链不仅可以基于规格自动构造测试用例,并有SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
openJML可以用来编译含有JML规格的代码。使用SMT Solver来对程序检测是否满足所设计的规格。
JMLUnitNG可以自动生成对应的JML测试样例,进行测试。
部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果
pass
JMLUnitNG/JMLUnit测试,并结合规格对生成的测试用例和数据进行测试
通过使用简单的方法进行测试,测试过程如下
首先编写一个简单的测试用例,本例中可以看出SubClass的规格是没有考虑到边界条件的
public class TestJML {
public static void main(String[] args) {
SubClass sub = new SubClass();
System.out.println(sub.subclass(12345, 23456));
}
}
public class SubClass {
/*@ public normal_behaviour
@ ensures \result == x - y;
*/
public int substract(int x, int y) {
return x - y;
}
}
两个文件同放置在test文件夹下。
通过 java -jar jmlunitng.jar test/TestJML.java test/SubClass.java生成测试文件,通过javac -cp jmlunitng.jar test/TestJML.java test/SubClass.java 和 openjml -rac test/TestJML.java test/SubClass.java命令进行编译。
测试:
java -cp jmluniting.jar: SubClass_JML_Test
测试结果如图所示

可见,正是因为没有考虑输入如边界问题,导致产生了三个失败测试。
进行修改
public class TestJML {
public static void main(String[] args) throws Exception {
SubClass sub = new SubClass();
System.out.println(sub.subclass(12345, 23456));
}
}
public class SubClass {
/*@ public normal_behaviour
@ requires x > 0 && x < 50 && y > 0 && y < 50;
@ ensures \result == x - y;
@ also
@ exceptional_behavior
@ signals (Exception e) x <= 0 || x >= 50 || y <= 0 || y >= 50;
*/
public int substract(int x, int y) throws Exception {
if (x > 0 && x < 50 && y > 0 && y < 50)
return x - y;
throw new Exception("wrong");
}
}
测试结果如图所示

可见,对规格进行修改之后代码的预期可控,测试也就自然而然的全部pass了
进行一些复杂的测试
测试代码
public class MyPath {
//@public instance model non_null int[] n;
private int[] nodes;
public MyPath(int[] nodeList) {
nodes = nodeList;
}
//@ ensures \result == n.length;
public /*@ pure @*/ int size() {
return nodes.length;
}
/*@ requires index >= 0 && index < size();
@ assignable \nothing;
@ ensures \result == n[index];
@*/
public /*@ pure @*/ int getNode(int index) {
return nodes[index];
}
// @ensures \result == (n.length >= 2);
public /*@ pure @*/ boolean isValid() {
return size() >= 2;
}
public static void main(String[] args) {}
}
自动生成的测试结果为

实在搞不懂为什么会有两个构造函数的failed,难不成是我没实例化对象???这难道不应该是openjml干的吗?经过简单的测试发现,openjml对于简单的方法测试还是可以做到的,如第一个测试。在难点就并不是十分友好(也可能是我没有掌握要领。。)。
架构设计
三次作业还是次次重构,hhh。并不是因为重构爽,是因为前两次代码都有bug,并且设计的有些混乱所以必须得重构,唉。第一次作业是因为掉以轻心,以为这次作业就是练习写规格,没有考虑时间复杂度的问题。导致每次在算节点个数的时候都是暴力搜索。。。第二次作业虽然考虑了时间复杂度,但是在设计的时候还是存在了bug,在存储边的时候我并没有按照邻接矩阵的方法存储,而是对边进行hashmap存储,然而边的个数实在是太多,所以实际上时间复杂度并没有降下来,总之设计的确实有点问题。最后一次作业终于AC了,也就没什么可分析的了。就是先计算每个直达图的最短路径之后在整合所有边计算整体的最短路径。
下面是具体的度量分析
第一次作业
代码很简单,直接通过ArrayList存储就妥了(然而时间超了。。。)


第二次作业
本次作业要求新增一个Graph类,并实现最短路径算法。本次代码使用hashmap存储。每个相同的边记录次数,当边彻底被删光(次数为0)时更新整个图的节点和最短路径(Floyd算法)。度量信息如下



第三次作业
这次作业继续先重构。。。由于上次想骚一把存的是edge而不是矩阵,结果就被自己玩死了,尝到了不及格的滋味。。。这次就稳了一手。通过学习讨论区的先进经验,最终选择先计算每个直达图的最短路径之后在整合所有边计算整体的最短路径。至于插点的方法,当时感觉复杂度太高,可能会死,就没用。实施证明确实如此。




Bug分析
前两次都是时间复杂度问题,第三次AC。具体也没啥可分析的,主要是算法问题,不是规格导致。
总结
JML确实是一个很有用的工具,用的好的话可以很好帮助程序员之间合作开发,甚至都不需要注释来表明自己的意图。但规格的编写实际也并不是一件容易的事情。这一单元的主要收获是更加了解了编程的规范,和良好的设计思路。谢谢oo教会了我这么多东西!!!

浙公网安备 33010602011771号