【作业】BUAAOO第三单元博客作业

【作业】BUAAOO第三单元博客作业

规格撰写作业总结

一、JML语言概况

JML(Java Modeling Language)是一种精确的形式规范描述语言,能准确表达方法的功能需求,并且JML在形式规范的基础上,可以利用自身开发的工具进行高效率的单元测试。
JML能够规范整个代码结果的正确性,但前提条件是JML语句本身的正确性。JML并未限制代码形成的过程,而仅仅是对结果做了规定,这样可以使代码更加多样化,以便寻找更加高效的算法。

(一)JML语言的构成

  1. 注释结构

    JML是以javadoc注释的方式来表示规格的,具体形式如下:

     // @ Single line annotation
    
     /*@ Multiple lines of annotation
       @ ...
       @ ...
       @*/
  2. 表达式结构

    1. 原子表达式
      • \result:一个非void类型方法执行后所得的结果。JML没有值变换的行为级描述,只有通过对结果的描述才能表示代码执行的预期。
      • \old:表示表达式在相应方法前的取值。注意包含内容与范围不同,所表达的结果可能也不同
      • \not_assigned(x,y,...):用来表示括号中的变量在方法执行的过程中是否被赋值,没有返回true。
      • \not_modified(x,y,...):表示变量取值是否发生变化。
      • \notnullelements(container):表示container对象中没有对象是空。
      • \type:查看类型并返回类型所对应的class。
      • typeof:返回表达式的准确类型。
    2. 量化表达式(常用)
      • \forall:全称量词修饰的表达式,表示对范围内所有元素都应满足的约束。
      • \exists:存在量词修饰的表达式,表示范围内存在马满足约束的元素。
      • \sum:求和。
      • \product:范围内元素的连乘结果。
      • \max, \min:最大值和最小值。
    3. 集合表达式:相当于数据结构容器。

    4. 操作符:
      • <:子类型关系操作符。
      • <==>:等价关系。
      • ==>:推理操作符。
      • \nothing:表示空集。
      • \everything:表示全集。
  3. 方法规格
    1. 前置条件requires P:表示调用者要确保P为真。
    2. 后置条件ensures P:表示方法实现这要保证结果满足P。
    3. 副作用
    4. signals子句:用于抛出异常。
  4. 类型规格

    1. 不变式invariant:所有可见状态下都必须满足的特性

    2. 状态变化约束constraint:满足约束的一类不变式。

(二)应用场景

  1. JML工具链
    JML有标准的撰写规格,所以可以对其格式与语法进行检查,如OpenJML静态检验。
    还可以通过JML构建自动化测试样例,并且通过JUnit等插件进行测试与结果断言比对。

  2. 应用情况
    目前,JML已经在市场上有所应用,鉴于其良好的模块结果规范方法,并且可以通过工具链生成自动测试的特性,越来越多的公司选择JML作为开发的规范与验证方式。

二、JMLUnitNG/JMLUnit测试

在测试前首先要更改规格描述与代码中的变量,使他们一致。
之后编译。由于我的java版本合适,所以没有配置java环境。
之后运行jmlunitng即可测试。测试结果如下:

三、三次作业架构梳理

(一)第一次作业

  1. 代码实现
    第一次作业的代码相对简单,只要按照规格提供的规定来写就可以保证正确性,但是难以保证性能。

  2. 架构优化
    为了使查询更加迅速,而不是在每次查询时都要遍历PathContainer,我使用了被同学们广为接受的双HashMap结构。建立了PathId到Path的映射,同时还包含了一个记录所有节点出现次数的nodes结构,用于返回不同节点数目。由于没有建立Path到PathId的映射,所以在查询是否包含某一条Path时,我调用的是HashMap的containsValue方法,复杂度比在KeySet查询中要高不少,这也是我设计上的失误。

  3. 复杂度分析
    类间关系如下图

第一次作业没有实现私有方法,所以Path与PathContainer两个类的复杂程度基本相似。

(二)第二次作业

  1. 代码实现

第二次作业加入了Graph类的各类查询方法,所以涉及到了不少算法方面的问题。

在第一版作业中,我使用了Floyd算法来查询最短路。我构建了两个新的数据结构,一个用来存储邻接矩阵,一个用来存储最短路。这两个结构都是嵌套式的HashMap,用来模拟二维数组,并且实现更快的查询操作。

但是由于我对Floyd算法使用的不够熟练,所以没有能力进行优化,在每次加边或删边时都需要更新两个矩阵,并且需要重新构造邻接矩阵,以O(n2)的复杂度初始化邻接矩阵,再以O(n3)的复杂度构建最短边矩阵。这样的实现使得复杂度极高,与同学们比,运行速度很慢,甚至时间能达到四到五倍,缓存失效的测试点甚至会爆点。于是我做了如下优化。

  1. 架构优化

提交截至前的下午,我极限操作改用了bfs算法,并且在加边操作时清空最短边矩阵,再重新构造,减边时删除该路径上所有边,更新邻接矩阵,再bfs。通过这样的优化,我的代码终于可以和大佬们同台竞技了。。。

  1. 复杂度分析

本次作业的Graph类直接在PathContainer类的基础上实现,由于没有采用继承的方式,所以复杂方法主要集中在Graph类。

(三)第三次作业

  1. 代码实现

第三次作业是在前两次作业的基础上搭建一个简单的地铁信息查询系统。由于加入了三种最短路,即对边权进行了修改,所以bfs算法与Floyd算法直观上都无法使用了(如果不拆点的话)。加入了最低票价、最小不满意度、最少换乘三种最短路,所以我添加了三个类似于之前最短路的HashMap嵌套结构,目的同样是减少查询复杂度。

由于我太菜了,所以没有想到如何使用更好的方式进行图的构建,也不会拆点的方式,所以借鉴了讨论区中葛毅飞同学设置边权的做法,在此感谢这位同学救了一命!

引用葛同学的做法:

对于LeastTransfer,构建一个图,首先把一个Path中所有边之间的weight全部设置为0,为体现换乘数,再将所有weight为0的边权值加1得到finalWeight。这样的话,我们从节点i1到节点i2的搭乘线路数就是以finalWeight为权值的图的最短路径,最小换乘数=最短线路数-1.

对于LeastPrice,构建图,首先把一个Path中所有边的weight如楼主设置(x),为体现换乘数,再将所有设置过weight的边权值加2得到finalWeight(2y)。最低票价 = 以finalWeight为权值最短路径 - 2。

对于LeastUnpleasantValue,构建图,首先把一个Path中所有点连通,边之间weight设置为实际从一站到另一站的总unpleasantValue,将所有设置过weight的权值加32得到finalWeight。最低不满意度 = 以finalWeight为权值最短路径-32。

  1. 代码重构
    为了使用边权,我改用了Dijkstra算法。但是对于不同的最短路,每次增删都需要将邻接矩阵与最短路矩阵清空,并且还要在正式查询之前将矩阵构建成每一条路径独立、仅靠连接点联合各路径的特殊形式,又需要再做一次Dij预处理,复杂度很高。由于没有熟练使用Dij,并且邻接矩阵的构造出现了不相连节点边权为空而不是极大值的问题,导致Dij时出现了没有找到最短路的问题,使得中测强测双双凉凉。也算是吃一堑长一智吧。

  2. 复杂度分析
    为了降低复杂度,我把PathContainer搞回来了,并且单开了一个类用来存储Dij方法,实现了对最低票价和最小不满意度的接口,所以复杂度较高,并且较为集中在Graph类中。

四、bug巡查与修复

前两次作业中没有发现bug,但是可能会存在潜在的bug。第三次作业由于Dij邻接矩阵的构造出现了不相连节点边权为空而不是极大值的问题,导致Dij时出现了没有找到最短路的问题。

五、心得体会

JML是一个很棒的语言,规定了规格,使得开发的效率大大提高。但是老师也说过,写规格的时间,一般是完成代码时间的三倍以上,所以可见开发的不易。总的来说,本次实验代码量不大,所以可以有更多时间保证代码的质量。希望下个单元的练习也能像这次一样。

猜你喜欢

转载自www.cnblogs.com/dzlbuaa/p/10908365.html