2019_BUAAOO_第三单元总结

2019_BUAAOO_第三单元总结

一、梳理JML语言的理论基础、应用工具链情况

​ JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。

以下是JML的常用语法

  • \result 用来表示一个非void类型的方法执行的返回结果。

  • \old(expr) 用来表示在相应方法执行前,表达式expr的取值。

  • \not_assigned(x,y...) 用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true,否则返回 false 。

  • \not_modified(x,y...) 限制括号中的变量在方法执行期间的取值未发生变化。

  • \forall 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

  • \exists 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

  • \product 返回给定范围内的表达式的连乘结果。

  • \max 返回给定范围内的表达式的最大值。

  • \min 返回给定范围内的表达式的最小值。

  • \num_of 返回指定变量中满足相应条件的取值个数。

  • \sum 返回给定范围内的表达式的和。

  • assignble 表示可赋值。

  • modifiable 表示可修改。

  • requires 前置条件,要求调用者确保其后的谓词为真。

  • ensures 后置条件,方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真。

  • signals 的结构为signals (...Exception e) b_expr,意思是当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e。

  • a ==> b a推出b。

  • a <== b a隐含b。

  • a <==> b a当且仅当b。

JML的应用工具

​ JML的应用工具链不算太广,比较常见的是OpenJML,JMLUnitNG等。大部分应用工具都是用来提供对JML的编译和检查,也有的工具可以通过JML来生成相应的测试代码。

二、部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例, 并结合规格对生成的测试用例和数据进行简要分析

​ 根据讨论区伦佬的帖子,下载了JMLUnitNG的jar包,并利用简单的demo代码(比较两个数的大小)来自动生成测试样例如下。(膜伦佬)

// demo/Demo.java
package demo;

public class Demo {
    /*@ public normal_behaviour
      @ ensures \result == lhs - rhs;
    */
    public static int compare(int lhs, int rhs) {
        return lhs - rhs;
    }

    public static void main(String[] args) {
        compare(114514,1919810);
    }
}

​ 生成的测试数据如下:

​ 可见,生成的数据均为int范围边界的数据,并且有三组由于相减爆int而出错的failures。

​ 然后我进一步进行测试,改了一个功能较多的demo如下:

// demo/Demo.java
package demo;

public class Demo {
    /*@ public normal_behaviour
      @ ensures \result == a + b;
    */
    public static int plus(int a, int b) {
        return a + b;
    }

    /*@ public normal_behaviour
      @ ensures \result == a - b;
    */
    public static int minus(int a, int b) {
        return a - b;
    }

    /*@ public normal_behaviour
      @ ensures \result == a * b;
    */
    public static int mult(int a, int b) {
        return a * b;
    }

    /*@ public normal_behaviour
      @ ensures \result == a / b;
    */
    public static int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        plus(114514,1919810);
        minus(24214,2414);
        mult(2442,142);
        div(414212,2);
    }
}

​ 生成的测试数据如下:

​ 可见生成的数据还是在int范围的边界。由此我的猜测与分析是,jmlunit生成的自动测试数据,主要是对边界情况进行分析与测试,更好的测试了程序的覆盖面是否全面。

三、按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构

​ 在三次作业中,我所存的矩阵的形式均为静态数组。因为在指定位置存储或者查询的复杂度都只是O(1),并且指导书对最大节点的大小有规定,所以只需要将二维静态数组的开到所规定的上限即可。

​ 在第一次作业中,由于程序的构造十分的简单,所以我只是按照指导书上所说,构造了必须要有的两个类MyPath和MyPathContainer。但是在一开始写的时候,我使用了ArrayList的动态数组结构来存路径和节点,在看了讨论区的帖子并上网查询资料之后,得知ArrayList的插入(在指定位置)、查找和删除的复杂度为O(n),在第一次作业中的查询distinctNodeCount指令的时候,需要嵌套多层循环,时间复杂度高达O(n^3),确实感觉这种方法不太可行,所以改用了Hashmap,在查询的时候可以大大的减少复杂度。

​ 在第二次作业中,涉及到了图结构,我复制了第一次作业的pathContainer代码,然后对新增的要求进行增添,而没有重构。由于作业的架构比较清晰,所以我还是只构造了MyGraph和MyPath两个类。判断两个节点是否存在一条边的指令,只需要在add和remove的时候,对相邻两个节点在Edge矩阵中所处的位置下标,所指的那个元素进行更新即可(为0表示不存在边,为1表示存在边)。在本次作业中,难点为getShortestPathLength指令,处理整个指令的算法有很多。我是用的是Floyd算法,因为add和remove的指令条数有限且很少,所以只需要在add或者remove之后使用一边floyd来更新最短距离的矩阵,更新之后对于任意两点的最短距离,都可以通过二位静态数组一下取出。所以对于该指令的查询复杂度,也只是O(1),如果将floyd放到该指令的方法中,会导致复杂度乘上上千倍的系数(因为该查询最短路径的指令可以有上千条),会很容易出现TLE的情况。

​ 在第三次作业中,我基本上是在上一次的作业中进行修改添加,没有重构。一开始我想使用拆点的方法,来对不同路径上的相同站进行一个区分处理。但是经过思考之后觉得该方法很复杂,并且会有很大的内存开销,也不保证时间复杂度不会超出范围。所以我思考了其他的算法,但是也没有特别好的解决方法。在这里感谢讨论区的王嘉仪、葛毅飞、孙一丹等大佬对她们所使用的算法(应该是相同的)的分析和解释。阅读了她们的想法之后,我觉得她们的算法十分的巧妙,很好的利用数学上的算法来解决了一个复杂的问题。具体的方法不做阐述,可以参考第十一次作业的讨论区。

​ 大概的思路是,每次添加路径的时候,先求出该路径上的四个“最短”的指标(权值),要加上换乘所需要的消耗(如不满意度32,换成票价2,换乘次数1),存入矩阵中。然后如果查询的两个节点不处于同一条路径,那么就可以使用最短路算法(在这里我沿用了第二次作业的Floyd算法),求出该两个节点相应权值,由于第一次乘车不算换乘,所以还需要再减去一次换乘所需要消耗的权值(如不满意度32,换成票价2,换乘次数1)。这种算法的好处是,很好的避开了我们对换乘所需要做的具体分析,而是通过添加换乘所需要消耗的权值来隐含表现,大大减少了工作量。

​ 但是我发现,如果每次add或者remove都重新跑floyd的话,跑一组极端数据(不知道哪位大神提供的)需要10多秒,但是其他同学的代码只需要4-5秒。原因是由于我在add和remove的时候没有很好的利用前面已经构好的图,每次都要重新构图,所以大大增加了时间开销。于是我想到了一个优化的方法,建立一个Edges类,然后在这个类中声明了两个LinkedList成员,values这个链表中存的元素是有序的,由小到大存储的,某一个路径上的一个边的权值。pathId链表中存的是路径的id,存储的位置和values相对应,便于删除和查找。然后在地铁系统的class中建立四个二维数组Edges[120][120],其实本质上有点类似于三维数组,可以想象成在120*120的棋盘,每一个棋盘块有一个与棋盘平面垂直的linkedList,其中有序的存好了权值

​ 进行如此的架构调整,只需要在每一次add的时候,求出一条路上的最小权值后,存入linkedlist,并且排序好,在floyd的时候,不需要重新构图,只需要取出每一种最短路权值的linkedlist的首元素(当前两个边的最小权值),进行floyd运算,保证复杂度停留在O(n^3),可以大大的减小时间上的开销。在remove的时候,也只需要遍历链表,将pathid为删除路径的id所对应的位置的权值元素删掉,这样还可以保证链表的首元素是两条边的最小权值。经过这种优化处理后,再跑一次极端数据,时间大概消耗在4秒左右。

四、按照作业分析代码实现的bug和修复情况

​ 在本单元的作业中,强测和互测环节都未被发现bug。但是在写代码时出现了一些bug,在提交之前及时发现并进行修正。

  • 查询节点自己到自己的最短距离、最少换乘等等。一条路径1-2-3,查询路径2-2的最小换乘,如果不做特判的话,程序会输出-1。因为对于所存的最小换乘的矩阵,对角线的值均为0,所以用前述方法来做,输出为矩阵中该元素的值减一,即0-1=-1。处理方法为:在查询时作特判,若 fromnode == tonode 则输出0。
  • 结点到数组下标的映射。由于使用了hashmap的存储结构来存储路径,所以在Floyd算法处理最小距离的时候,应该只对存在映射的数组下标进行遍历。由于一开始我对整个大矩阵都进行了遍历,有一些不存在映射的数组下标也参与了计算,所以会导致在remove的时候,若没有对可达矩阵进行更新(如赋值int最大值),会出现remove之后还可以存在最短路径的问题,但其实此时两个点已经不可达。
  • 运行时间过长(不确定是否会TLE)。在前述第三次架构中,有提到如何大大减少add、remove指令时会产生的重新floyd构图导致的O(n^3)~O(n^4)的高时间复杂度,将每一条路径的信息尽可能地使用到,并且有序地存好,在查询使用的时候可以尽可能地减少工作量。
  • 未严格的读JML。对于某些异常的处理,由于我只是注重了normal_behaviour,所以对于节点、路径不存在的情况要抛出异常的处理,没有进行判断,所以会导致输出结果不对,甚至出现抛出异常,并且无法输出语句的情况。
  • Floyd算法的问题。在对几个最短矩阵进行初始化的时候,我统统对其赋上int的最大值。在某一次跑代码的时候,发现最短路径、最少不满意度等的查询指令的输出接近于int的负数极限,于是在debug的时候,发现了Floyd()方法里问题——通过节点i,来计算j,k的最短路径的时候,如果i,j不可达并且i,k不可达,则会导致二者相加(两个int最大值)爆int范围,求得的值为负数,负数小于j,k当前的路径长度,所以会用负数来更新距离矩阵。出现该问题的原因是因为在更新最短路径的时候,首先要进行判断两点是否可达,如果不可达的话应直接continue循环,而不是继续运算。解决方法除了判断可达之外,还可以在初始化矩阵的时候,不赋值成int的最大值,而是再小一些,至少应保证两个值相加不超出int范围。
  • Nullpointer问题。经常会忘记对数组、链表等进行初始化。

五、阐述对规格撰写和理解上的心得体会

​ JML规格,很好的串联起了用户需求和程序员之间的联系。在撰写JML规格的时候,每一步语句、表达式都能很严谨了说清楚该方法的所有具体操作。其中,我发现要想写出一个准确的、清晰的JML规格,对离散数学的知识要求是要有的,里面有很多逻辑上的表达式或运算符等,都是以离散数学为基础。对于简单的方法来说,JML可以很好的表达出该方法的操作,但是如果复杂一点(比如第三次作业的地铁系统中的方法)的话,阅读JML是十分困难的,需要一定的时间和耐心。

​ 如果掌握好了规格的阅读和撰写能力,会大大完善自己的架构能力,并且在以后的项目开发等方面,会有事半功倍的效果。

posted @ 2019-05-22 12:09  zja1999  阅读(145)  评论(0编辑  收藏  举报