面向对象第三单元总结

 

概述

  本单元主要是依靠JML规格,编写能够完成规定功能的代码,并且在完成功能的基础上,对复杂度有一定的要求,要尽可能地降低和分散复杂度,使得程序运行速度尽可能快。本单元共有三次作业:

  • 实现两个容器类Path和PathContainer,对JML规格进行初步的理解和代码实现
  • 实现两个容器类Path和Graph,对JML规格进一步理解,代码实现和设计模式的简单实战,以及初步尝试单元测试
  • 实现两个容器类Path和RailwaySystem,对JML规格进一步理解,代码实现、设计模式和单元测试有更高的要求

一、JML

   JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种工具,是行为接口规格语言,可用于指定Java模块的行为。对于调用者,只需要根据规格就可以知道方法调用后所产生的结果及其影响,而不需要去考虑其具体代码如何实现;对于开发者,只需要根据规格实现满足规格的代码即可,不需要对输入输出进行过多的考虑。JML常常被用于两个方面,一方面是设计规格,实现代码;另一方面是对已有的代码,书写规格,提高代码的可维护性。

1、理论基础

注释结构

  JML以javadoc注释的方式来表示规格,每行以@起头。其中行注释为“//@annotation”,块注释为“/*@ annotation... @*/”

  除此之外,JML一般都有requires子句、副作用范围限定和ensures子句。requires子句是定义该方法的前置条件,这是对调用者的一些要求,如果想要调用这个方法来得到预计的结果的话,那么需要满足requires子句中的条件才可以;副作用范围限定,这个是列出这个方法执行后,会对哪些类成员进行修改,\nothing代表不会对任何成员进行修改,也表示是个pure方法;ensures子句定义后置条件,这是对方法实现的要求,这个方法要是的ensures后面的子句条件为真。

  需要注意的是,规格中的每个子句必须以分号结尾。

JML表达式

  JML表达式是对Java表达式的扩展,其中比较常用的包括\result、\old、\forall、\exists等。

  • \result:表示一个非void类型的方法返回值。
  • \old(expr):表示expr在方法执行前的值。值得注意的是,当expr是一个对象引用的时候,那么只要expr没有发生改变,那么\old(expr)指向的还是引用后的对象,\old(expr).size()和expr.size()永真,如果想要获得对象在方法调用前的一些属性,应该使用\old(expr.size())类似的语句。
  • \forall:全称量词修饰的表达式,表示对于给定范围的元素,都应该满足约束。
  • \exists:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足约束。
  • 等价关系操作符:expr1<==>expr2或者expr1<=!=>expr2,表示expr1==expr2或者expr1!=expr2,优先级比==和!=低。
  • 推理操作符:expr1==>expr2,当expr1==false或者expr1==true且expr2==ture时,整个表达式为true。
  • \max、\min:返回给定范围内的表达式的最大值、最小值。
  • \sum:返回给定范围内的表达式的和。
  • \num_of:返回指定变量中满足条件的个数。

方法规格

  方法规格除了上面所提到的前置条件、后置条件、副作用范围限定,还有更高一级的区分,就是正常行为规格(normal_behavior)和异常行为规格(expcetional_behavior)。正常行为规格对应着正常的输入输出,而异常行为规格则用于应对用户的所有可能输入。当一个方法规格中涉及多个功能规格描述,或者父类已经定义了规格,子类重写的该方法,需要补充规格,需要在规格前面或者之间加入also来分隔。通常在异常行为规格中,后置条件用来抛出异常,需要使用signals子句来表示。

类型规格

  • invariant:不变式,是要求所有可见状态下都必须满足的特性。
  • constraints:状态变化约束,对象的状态在变化时需要满足的一些特性。

2、应用工具链

  • OpenJML:可以对规格进行规范检查, 也可以对代码进行静态和运行时检查。
  • JMLUnitNG:是用于JML注释的Java代码的自动化单元测试生成工具。

二、部署JMLUnitNG

  部署JMLUnitNG耗费了很多时间,可能是因为头铁,总是想要在Windows10下面配置的缘故,总是有些bug等着我。

  首先由于工具的一些问题,就只好把MyPath删减了一些,写出了一个简化版的MyPath类,去掉了所有的\exists和\forall,具体代码如下:

 1 import java.util.ArrayList;
 2 import java.util.HashSet;
 3 
 4 public class MyPath {
 5     private /*@ spec_public @*/ ArrayList<Integer> nodes;
 6     private /*@ spec_public @*/ HashSet<Integer> set;
 7 
 8     public MyPath(int... nodeList) {
 9         nodes = new ArrayList<Integer>();
10         set = new HashSet<Integer>();
11         for (int i = 0; i < nodeList.length; i++) {
12             Integer a = nodeList[i];
13             nodes.add(a);
14             set.add(a);
15         }
16     }
17 
18     //@ ensures \result == nodes.size();
19     public /*@pure@*/ int size() {
20         return nodes.size();
21     }
22 
23     /*@ requires index >= 0 && index < size();
24       @ assignable \nothing;
25       @ ensures \result == nodes.get(index).intValue();
26       @*/
27     public /*@pure@*/ int getNode(int index) {
28         return nodes.get(index);
29     }
30 
31     //@ ensures \result == set.contains(i);
32     public boolean containsNode(int i) {
33         return set.contains(i);
34     }
35 
36     //@ ensures \result == (nodes.size() >= 2);
37     public boolean isValid() {
38         return nodes.size() >= 2;
39     }
40 
41     public static void main(String[] args) {
42         ;
43     }
44 }

 

  其中存储节点的数据结构还是使用ArrayList,所以在原来规格中的nodes.length都要给成nodes.size(),同时nodes[i]也要改成nodes.get(i).intValue()才能够避免报错。

  然后当前目录结构为

  然后执行以下指令:

1 java -jar .\jmlunitng.jar .\src\MyPath.java
2 javac -cp .\jmlunitng.jar .\src\*.java
3 java -jar .\openjml.jar -rac .\src\MyPath.java

 

  到这一步,运行还是没有太大问题,文件目录结构如下所示:

│  jmlunitng.jar
│  openjml.jar
│
├─out
└─src
        MyPath.class
        MyPath.java
        MyPath_ClassStrategy_int.class
        MyPath_ClassStrategy_int.java
        MyPath_ClassStrategy_int1DArray.class
        MyPath_ClassStrategy_int1DArray.java
        MyPath_ClassStrategy_java_lang_String.class
        MyPath_ClassStrategy_java_lang_String.java
        MyPath_ClassStrategy_java_lang_String1DArray.class
        MyPath_ClassStrategy_java_lang_String1DArray.java
        MyPath_containsNode__int_i__0__i.class
        MyPath_containsNode__int_i__0__i.java
        MyPath_getNode__int_index__0__index.class
        MyPath_getNode__int_index__0__index.java
        MyPath_InstanceStrategy.class
        MyPath_InstanceStrategy.java
        MyPath_JML_Test.class
        MyPath_JML_Test.java
        MyPath_main__String1DArray_args__10__args.class
        MyPath_main__String1DArray_args__10__args.java
        MyPath_MyPath__int1DArray_nodeList__0__nodeList.class
        MyPath_MyPath__int1DArray_nodeList__0__nodeList.java
        PackageStrategy_int.class
        PackageStrategy_int.java
        PackageStrategy_int1DArray.class
        PackageStrategy_int1DArray.java
        PackageStrategy_java_lang_String.class
        PackageStrategy_java_lang_String.java
        PackageStrategy_java_lang_String1DArray.class
        PackageStrategy_java_lang_String1DArray.java 

  出现很多java文件和class文件,但是当我们运行时会出现无法找到主类的错误,这时候我们可以将这些java文件复制到idea中,在idea中运行MyPath_JML_Test即可,需要注意的是,要将jmlunitng.jar文件添加到项目中。

  运行结果如下所示:

  其中我们发现getNode方法出现的问题,虽然我们有requires,但是工具似乎并没有理我们,依旧出现了负数之类的测试。然后我们对这些方法进行了一些修改,得到了如下的结果:

  感觉整个测试样例似乎就是在测试整数溢出,对象是否为空等一些语法上的东西,似乎并没有测试一些方法逻辑上的东西,也有可能是代码太简单了,测试不出什么东西。

三、作业架构设计

第一次作业

  第一次作业比较简单,并没有什么需要架构设计的东西,完成两个需要的类就好了,其中MyPathContainer为了查找性能的提升,我加入了两个HashMap,PathId和Path的相互映射,使得getPathById和getPathId速度能够快一点,而且还加入了nodeToNumber的HashMap,来保存容器内有多少个不同的点。

第二次作业

  因为这一次作业的要求是对第一次作业的继承,所以这次作业我对第一次作业进行了继承,同时重写图变更函数,在不改变MyPathContainer的基础上,加入了对边的统计,在每次图变更之后,就计算各点之间的距离矩阵,同时使用Floyd算法来实现最短路径的计算,这个方法实现较为简单,而且复杂度也是可以承受的。

  同时因为nodeId是在int范围内,而图内的最多节点个数不超过120个,所以距离矩阵最大不超过120*120,需要一个nodeId到距离矩阵index的映射,这同样也是在图变更的时候实现。而判断连通性则根据两点之间的最短路径长度来判断。这次作业的代码量并不大,主要是在实现Floyd算法上面,而且这个算法也不算难。

第三次作业

  这次作业处理方式和第二次作业一样,都是继承上一次作业来继续写。这次的getLeastTickerPrice、getLeastUnpleasantValue和getLeastTransfer和第二次作业最短路径类似,只是需要考虑转乘的问题,可以通过拆点的方式,例如最小换乘可以在换乘站点之间的权值为1而其他边的权值为0的方式,来使用Floyd来求解。但是拆点可能使得距离矩阵变得非常巨大,如果针对这个来构造指令的话,那么距离矩阵会有4000+个点,Floyd的运算速度很慢,造成计算效率很低,所以我才用Dijkstra算法来解决这一问题。

  首先我们对每个节点拆分成x+2个点,x为出现的路径总和,附加两个点为起点和终点,和这x个点有着一条有向边。然后我们最多只需要计算着120个起点到其他点的最短距离,复杂度降到了120*n^2,相比于n^3的复杂度,程序速度会快很多。而且Dijkstra是单源最短路径的算法,所以我们不需要一次性计算出所有的最短路径,只计算出需要的即可,如此我们可以增加一个HashMap,用来标记是否计算了该Node的最短路径,如此复杂度可以进一步降低。

  在计算连通块个数的时候,我们可以通过循环来对所有点进行标记。因为MyGraph已经提供了点与点之间是否相连,所以我们可以一次取出一个未标记的点,然后遍历所有点来进行标记,一次可以标记一个连通块,所以循环n次就代表有n个联通块。

  这次作业中,Dijkstra算法是一个重复的部分,大概需要计算三次,所以可以把这部分逻辑提取出来,做一个方法出来,但是我并没有提取出一个图计算类出来。如果提取一个图计算类出来,那么整个代码的耦合度能够降低许多,而且第二次作业的图计算也可以使用图计算类来实现。

四、代码实现的bug和修复

  三次作业中均没有出现bug,不过有了解一些其他人的bug。比如在comparaTo函数中,字典序比较两条路径,有人使用对nodeId相减的方式进行判断,这会导致整数溢出。

五、规格撰写和理解的心得体会

   规格撰写:规格的撰写用到了一些抽象,让我们去描述这些数据,在撰写规格的时候,只需要把方法当作一个黑箱,不需要考虑如何实现,只需要考虑前置条件、后置条件和副作用范围限定。对于我来说,简单的函数规格是比较容易写出的,而且有了规格,写代码也比较有方向。

  理解:规格表述出来的意思非常严谨,但是相对于自然语言,还是有些难懂,尤其是一层嵌套一层的时候,那么理解起来就非常麻烦,而且规格是为了表示前置条件和后置条件,但实际实现的时候,和规格可能就差很远,有时候为了提高效率还有可能使用其他的算法来实现。对于我来说,一般是先看指导书了解这个方法是在干什么,然后再看规格来摸清细节。在实际实现的时候,一般要先设计数据的组织方式,然后再根据数据结构来写代码,而且由于规格已经限定了输入输出,逻辑简单的就按部就班地实现就好了,较为复杂的就需要分为多个方法来实现。规格对于代码的实现还是有很大帮助的,但是让程序员去理解的话,还需要一些自然语言的描述来辅助。

  总的来说,JML对于实现上面肯定是有帮助的,但是目前来看并没有想象中的那么大,一方面是这个复杂逻辑的表示上面也是非常的复杂,对于程序员来说并不是那么友好,需要一些自然语言来辅助理解;另一方面,工具也不太利于使用,常常出现一些诡秘的bug,而且测试样例对于也局限于语法方面,对于代码逻辑还需要自行设计。但是JML非常严谨,对于程序员来说可以节省很多的精力用于处理输入输出,而是专心于代码的实现、复杂度的降低和bug的查找。

posted on 2019-05-22 15:53  Tywin  阅读(191)  评论(0)    收藏  举报

导航