oo第三单元博客

OO第三单元博客


一、JML

在第三单元的面向对象课程中我们第一次接触了JML语言以及基于JML规范的规格化设计。在之前一系列关于面向对象思想的学习认识中,我们知道了Java是一种面向对象的语言,面向对象思想的一个重要原则就是将过程性的思考尽可能地延迟。而作为Java建模语言,JML给予了我们一种全新的方式来看待Java的类和方法,其通过将一些符号语言显式地插入到Java程序代码中,来描述一个方法所要求的前提以及期望达到的效果,而将过程性的思考延迟到了方法的设计中。

JML理论基础

JML是用于对Java程序进行规格化设计的一种表示语言,是一种行为接口规格语言,基于Larch方法构建,BISL提供了对方法和类型的规格定义手段。使用JML可以帮助尽量推迟过程实现的思考,首先考虑要实现什么功能以及架构而不是怎么去干。

使用JML来声明性的描述一个方法或类的预期行为可以显著提高整体的开发进程,JML引入了大量用于描述方法行为的结构,将这些建模标记,以JAVA注释的方式加入到程序代码中,对正常编译没有影响,并且能够精确地描述代码、减少bug的出现以及规范用户对类与方法的使用。在实际设计过程中主要由两个用途:

  • 在实现前精确的表达设计的规格

  • 对实现的代码的逻辑进行梳理

    两方面都是利用了JML严谨的非自然语言规格以代替自然语言,以弥补自然语言表述上和通用性上不足。

JML可用于开展规格化设计,在代码工作开始前规范代码行为,也可针对已有的代码实现书写其相应规格,从而提高可维护性。

JML应用工具链情况

可以使用开源的JML编译器来编译含有JML标记的代码,如OpenJML,所生成的类文件会在运行时自动检查JML规范,若程序未实现规范中规定的事情,JML运行期断言检查编译器会抛出一个unchecked exception来说明程序违背了哪一条规范。JMLdoc工具与Javadoc工具类似,可在生成的HTML格式文档中包含JML规范,JMLUnit可以生成一个Java类文件测试的框架。SMT Solver工具可以以静态方式来检查代码实现对规格的满足情况。


 

二、SMT Solver

开源工具OpenJML已经包含了一些SMT Solver的方案,所以直接使用OpenJML来对程序的规格进行验证。

因为没有解决OpenJML测试\forall 和\exists的问题,所以只列出一些能够使用的

下面截取MyGraph类中一部分方法的问题:

1、addPath方法

警告出现超出int范围的情况,因为本单元对数据量的限制保护了该int类型的安全,不需要更改。

MyGraph.java:124: warning: The prover cannot establish an assertion (ArithmeticOperationRange) in method addPath:  overflow in int sum
           return id++;
                   ^

2、getShortestPathLength方法

这个返回路径的方法主要就是调用了Bfs计算方法,所以将Bfs的警告归到这里。

 MyPath.java:325: warning: The prover cannot establish an assertion (ExceptionalPostcondition) in method Bfs:
                  reachable = node.get(origin);

因为采用的图结构是邻接表实现,这里显示了如上的大量的可能存在的空指针错误,怀疑调用的HashMap调用出现空指针错误,出现这个问题不是特别明白为什么,可能是考虑到key值不存在从而导致错误(对照使用迭代器遍历key值访问HashMap的部分没有出现问题的情况)


三、JMLUnitNG/JMLUnit

有关JMLUnitNG的一些参数说明:

 调用JMLUnitNG,因为java -jar jmlunitng.jar [OPTIONS] path-listwhere path-list是一个以空格分隔的文件系统路径列表(到文件或目录),[OPTIONS]是0或更多以下命令行选项。除了JMLUnitNG生成的那些之外,递归地指定路径中的所有Java文件都根据命令行选项进行处理。
 -d, --dest [DIRECTORY]: DIRECTORY用作生成的类的输出目录。如果未指定,则在与测试的类相同的目录中生成测试类。
 -cp , --classpath:javac在解析过程中使用给定的目录列表和Jar文件(格式为 )作为类路径。默认情况下,使用CLASSPATH环境变量。
 -sp , --specspath:javac在解析过程中使用给定的目录列表和Jar文件(格式为 )作为specspath。默认情况下,使用SPECSPATH环境变量。
 --rac-version:为指定的JML RAC版本生成RAC处理代码。openjml对于OpenJML RAC ,默认值为' '。对于JML2和JML4 RAC(分别),其他支持的值是' jml2'和' jml4'。
 --deprecation:为不推荐使用的方法生成测试。使用@DeprecatedJava注释检测不推荐使用的方法(而不是在当前版本的JMLUnitNG中,通过 @deprecatedJavadoc标记)。默认情况下,此选项处于禁用状态,这意味着不会测试弃用的方法。
 --inherited:为继承的方法生成测试。默认情况下,此选项处于禁用状态,这意味着不会为其主体从父类继承的方法生成测试。打开它会导致为每个被测试的类中的所有(具体)方法生成测试。
 --public:仅为公共方法生成测试。这是JMLUnitNG的默认行为。
 --protected :为受保护和公共方法生成测试。
 --package :生成包(无保护修饰符),受保护和公共方法的测试。
 --parallel:生成默认为并行运行的数据提供程序。默认情况下,此选项处于关闭状态,这意味着相同方法的多个测试将按顺序运行(无论您的TestNG设置如何); 打开它可以使它们与适当的TestNG设置并行运行。
 --reflection:反射生成测试数据。默认情况下,此选项处于关闭状态,这意味着不会自动生成调用测试方法的对象; 打开它会导致生成这样的对象。
 --children:对于所有参数,不仅使用参数类而且还使用在生成测试时探索的该类的任何子级生成测试数据。这允许许多方法自动测试接口/抽象类参数。
 --literals :使用类和方法中的文字作为默认数据值来测试这些类和方法(在方法之外找到的文字,例如在静态字段中,用于所有方法)。
 --spec-literals :使用类和方法规范中的文字作为默认数据值来测试这些类和方法(类规范中的文字用于所有方法)。
 --clean:从目标路径中删除所有旧的JMLUnitNG生成的文件,包括任何手动修改。如果未设置目标路径,path-list则清除所有文件和目录。应谨慎使用此选项,因为它会在目标路径中以递归方式删除所有 JMLUnitNG生成的文件,或者 path-list无论何时/如何生成它们。
 --no-gen:不要生成测试。此选项通常与不需要的JMLUnitNG生成的文件一起使用 --clean或--prune删除,而不会生成新的文件。
 --dry-run:显示有关将执行但不修改文件系统的操作的状态/进度信息。当与任何其他选项集一起使用时,--dry-run使JMLUnitNG运行整个测试生成过程并显示步骤,但不生成输出; 它看到的文件将与被删除是有用的--clean或--prune,或者用什么方法将有选项特定集合为它们生成的测试。
 -v, --verbose :显示状态/进度信息。
 -h, --help :显示带有缩写文档的命令行选项列表

尝试探索JMLUnitNG的使用方法,对一个简易的代码进行测试(为作业中返回size和获得节点的方法)


 1 public class Demo {
 2      public ArrayList<Integer> nodes = new ArrayList<>();
 3      // @ public normal_behaviour
 4      // @ ensures \result = nodes.size();
 5      public /*@pure@*/ int size(){
 6           return nodes.size();
 7       }
 8       // @ public normal_behaviour
 9       // @ requires index >= 0 && index < size();
10       // @ assignable \nothing;
11       // @ ensures \result == nodes.get(index);
12      public   /*@pure@*/ int getNode(int index) {
13   
14          if (index < 0 || index >= size()) {
15              return -1;
16          }
17          return nodes.get(index);
18      }
19  }

 

1、生成测试文件

2、编译文件

3、使用jmlc编译文件

4、开始运行

具体在讨论区的大佬们发言中有详细步骤;

得到

 

发现进行的测试数据都是边界条件的测试。

再如之前打的简单的返回两个数之差的程序

可以认定其生成的自动测试数据为边界数据的测试。


 

四、架构设计

第一次作业只是简单的实现了满足接口实现的要求,简单粗暴打了两个类。

使用两】三个HashMap来完成存储和查找工作:

  • 使用HashMap<Integer, Path> 结构完成根据索引查找路径。

  • 使用HashMap<Path, Integer> 结构来完成根据路径查找索引。

  • 使用HashMap<Integer, Integer> 结构完成对当前容器内所有路径不同结点的查询,第一个 Integer 代表结点值,第二个 Integer 代表该结点在容器内的数目。每次增加路径时,该路径中结点对应的个数增加;每次删除路径的时候,将该路径中的每个结点对应的值减1,当出现次数变为0时在HashMap中 删去该结点。每次查询只需返回 key 的数量。

                第一次作业类图

第二次作业

第二次作业接口要求基本和上一次作业一样,考虑到JML只是对最后方法接口要求有限定,对实现过程没有限定,这里应该保持上一次的MyPathContainer封装不动,这次的Graph继承这个类以获取容器数据的,但是打的时候没仔细考虑就直接 Ctrl+A Ctrl+C Ctrl+V素质三连了第一次作业的Mycontainer的类,这个地方的处理这次是爽了,整个MyGraph集合了所有功能,但是这样的架构给我的第三次作业的过程带来了很大打击。导致需求更新后老架构不能用,直接重构火葬场。

其中图的结构采用一个HashMap < Integer, HashMap>,key值是节点id,value值是一个存储与其相邻的点的信息,具体是HashMap<Integer,Integer>,key值是记录节点id,value记录了该节点出现的次数。当一个节点不与任何节点相邻时从HashMap<Integer,HashMap>中删除key值,同理在HashMap<Integer,Integer>在remove中要更新value值,当value为0时删除key。

            第二次作业类图

第三次作业:

 

第三次作业的要求变成花式求最短路径,第二次作业没有好好想扩展,使用的是bfs找最短路径,并且缓存已经算出来的最短路径组成一个数据集,以求在大量计算的时候能够加速。的确在第二次作业中的效果很好;但是(记得有人说”但是“之前的全是废话来着)这次的加权重对bfs极其不友好,而且之前采用的缓存最短路径的方法也被这次的“神奇的换乘大冒险”操作变得无法重用;bfs的复杂度太高,最后是看讨论区里wjy大佬方法决定最后采用floyd算法,又不得不将图结构又加了一个邻接矩阵的形式去存;

整体架构:

我采用了Edge类用来存储一条边的必要信息(有关不满意度的信息也通过两端节点计算出来),之前打的MyGraph方法封装好,在原来的基础上构建出以邻接矩阵存储的图结构,将其传递给Flo(其中封装了有关Floyd的算法),用于计算具体数据,当需要在MyRailwaySystem中调用方法查询与计算时会调用Flo中的方法计算然后返回相应的值。

再读过标程之后才发现自己的代码架构真的差的很多,之前对泛型和工厂模式的用法有一定的了解但是自己从没有真正用在程序上,发现这样的操作模式对代码的可扩展性提升很大。

图结构:

本次需要的主要的图结构采用静态数组来存储,在Flo类中存储保存,用HashMap把节点id映射到数组下标之中,同时记录数组的那些下标是被使用的,那些是没有被使用的,在进行计算的时候只会访问已经被使用的下标。

           第三次作业类图


 

五、bug与修复

  • 第一次作业:

    题目比较简单,自己打完测试时没有发现问题,强测和公测也没有发现问题。互测的时候也没有发现其他同学的问题。

  • 第二次作业:

    自己随机测试没有发现问题,交上去以后测试出来对于重边的计数出现了问题,当一个点第一次出现,而且相邻的点相同时(如 path1:7 6 7)的计数只计了一次。所幸强测里面只有两个这样的点,分没被扣得太惨。

    在互测中找到了其他同学的问题,一个是有些采用Floyd算法的程序出现了在大量数据下超时的情况,可能没有怎么考虑优化的问题。还有一些计算错误……

  • 第三次作业:

    自己之前打的代码复杂度爆表了,然后开始重构,最后周二下午才莽完。就跑了个Junit的测试,没想到强测很幸运的没出现问题,互测里也没有被测试出问题。也没有测试出其他同学的问题。

六、对于规格的心得体会

使用规格是期望采用相对于自然语言来说更加严谨且没有二义性的方式来对程序的方法功能进行表述,自己的代码在测试的过程中没有发现BUG并不意味着没有BUG,单纯的使用大量测试数据无法保证完全正确,但是规格从逻辑层面的正确性证明保证了只要按照规格实现就不会出现问题。这意味着使用规格可以保证团队合作和较大规模的工程在保证正确性的同时更有效率。

在自己实践这种模式的时候,发现规格可以很便捷地客观地表述方法的接口和功能,但是仅限于描述一些功能比较简单的方法。当一个方法相对复杂的时候(或者方法的代码量过长)使用JML语言会显得十分冗长,且很难保证规格的逻辑没有漏洞。如第三次作业在实现规格的时候还需要借助本来不一定需要实现的方法,还要对这些方法单独撰写规格。所以感觉想要采用JML的时候需要将一个较为复杂的方法细化成很多个小方法。对撰写规格的人的架构能力有很高的要求。

posted @ 2019-05-22 09:46  lalala007  阅读(150)  评论(0编辑  收藏  举报