OO规格单元总结

第 三 单 元 J M L 总 结

PS: 关于这次JML单元

​ // JML大法好!

​ 这个单元的三次作业都是在基于给定描述规格下实现相应的接口,Path,PathContainer,Graph,和RailSystem。很明显是想让我们在熟悉JML的同时顺便学习一下迭代继承开发的过程,但是其实似乎并不是那么尽人意。

​ 主要的原因就是因为每次作业都有时间限制,虽然有些方法能够很简单的实现,但是为了不被TLE,不得不采用一些空间换取时间的做法,在规格之外定义了一些为了方法服务的数据结构,但是!下一次作业这些额外的数据结构和方法并不能很简单的被复用,更多时候因为有了新的要求和新的实现,老的数据结构显得很累赘过时。这就导致了我们不得不通过Ctrl+C/V来沿用上次的架构,而不是通过优雅的extends关键字。

​ 说实话这样感觉体验不是非常好,我问了许多同学好像也没有更好的解决办法。希望课程组以后这方面能够稍微改进一下,比如说允许使用protected访问限制(当然这样就得要求必须新建自己的包,但这也是个好编程习惯)。

JML基础

  • 语言理论基础:JML(Java Modeling language)的官网是这么形容自己的,一种行为接口规格语言。JML常用于开发人员间的交流,重规格而不在意实现。那么为了避免自然语言描述的二义性,JML自然是有一套完善的刑法语法。以Level 0为例:
    • JML表达式
      • 原子表达式
      • 量化表达式
      • 集合表达式
      • 操作符
    • 方法规格
      • Pre-condition
      • Post-condition
      • Side-effects
    • 类型规格
  • 应用工具链
    • Open JML:JML工具套件,允许静态检查JML规格的语法以及和SMT Solver配套动态检查代码对JML规格满足的情况。
    • JML SMT Solver:一般Open JML自带主流的SMT Solver,用途如上。
    • JML Unit:自动根据JML生成类文件测试框架。
    • JMLdoc

SMT Solver部署使用

下载并配置Open JML就可以使用目前主流的SMT solver来验证自己的程序啦。

配置Open JML

我使用了Idea自带的Extenal Tools配置使用Open JML,相当于直接使用命令行启动opem JML.jar。详细教程可见:IDEA外部工具配置-OPEN JML篇

  • -check进行语法检查:

  • -exec进行静态规格检查:

  • -rac进行动态检查:

使用结果

MyPath.java进行了语法检查,发现这个其实不是非常智能。按理来说规格只是对类的行为进行约束而不应该对类的实现干预,但是我使用了和规格容器不同的命名或者类型就会报error。

JML UnitNG部署使用

同样的,在External Tools设置中加入JMLUnitNG生成的配置。

对一个简单的java文件进行测试:

// 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);
    }
}
  • 生成Test样例:

  • 生成运行时检查:

  • 运行测试文件demo_JML_Test.java结果:

对三次架构的分析

Path接口

我三次作业都用的第一次作业的实现。架构中关键点有:

  • 求不同节点数:采用了额外的HashMap<Integer, Integer> nodes结构存<节点名,数量>对。

PathContainer接口

构架中关键的需求和实现:

  • public /*@pure@*/ boolean containsPath(Path path);

    public /*@pure@*/ boolean containsPathId(int pathId);

    因为JML规格中说明了PathId与Path都是唯一的,并且和上面两个函数一样的,对PathId和Path的查询和互换操作很常见,所以我采用了HashMap<Path, Integer> pathMap, HashMap<Integer, Path> idMap,分别存<Id, Path>对和<Path, Id>对的映射。通过双倍的空间代价节省了遍历容器的时间代价。

  • 求不同节点数:和Path接口的实现采用了同样的方法。

Graph接口

实现Graph接口的时候,图结构存在通过HashMap实现的邻接表。因为有了邻接表,原来用来求不同节点数的额外nodes结构也就不需要了。

构架中关键的需求和实现:

  • 求最短路:简单的BFS算法,算出来的结果(包括中间步骤求出的其他点最短路)存在一个最短路缓存结构HashMap<Tuple, Integer> pathCache // 其中Tuple是自己实现的类,存一个节点对。当图结构变更时清空pathCache
  • 求连通性:求连通性我直接计算最短路,当然也可以采用并查集。

RailSystem接口

地铁系统这个比较魔幻,新增了换乘这个概念。因为要计算最少换乘、最小票价、最小不满意度路径,如何根据换乘与否来确定边的权值大小就显得很关键。

我的方法是在dij算法上,优先队列中每个节点都对应一个存着当前所在路径的Id的HashSet A,当遍历邻接的点时,邻接的点也对应有一个存着这条边所在路径的Id的HashSet B,如果A∩B=∅,那么就判断需要换乘。

我这个办法比较笨,跑起来也没有优化之后的拆点算法快,但也算够用了吧。

构架中关键的需求和实现

  • 计算各个路径:继承了Graph的缓存思想,自定义一个SpecialPath类存各个特殊路径,用HashMap<Tuple, SpecialPath> pathCache缓存。求各个特殊路径的算法是普通的dij,根据求的类型定义不同的边权。
  • 求连通块个数:使用了并查集,连通块个数 = 总元素个数 - 合并次数。

分析代码的BUG以及修复情况

第一次作业实现compareTo方法时,顺手用了字典序比较字符串的方法:对应位字符相减。但是ascii码内的字符相减并不会溢出,但作业里int - int很明显会溢出。

修复自然就是不直接相减而是if语句判断一下返回±1即可。

心得体会

这次规格单元其实我并没有特别地去深度剖析每一个方法的规格规定,尤其是最后一次作业,为了定义求特殊路径的方法的规格,额外附加了好多规格又长又晦涩的辅助方法,我想完全贴合规格实现的人应该没有吧。

大多数情况其实是我们读了规格之后,把规格翻译成能理解的自然语言再动手实现,当遇到自己不知道如何处理情况时(比如说containsEdge这个方法如果输入两个相同nodeId返回值应该是什么,或者是求特殊路径的方法如果输入两个相同nodeId返回值应该是什么)再去仔细阅读规格寻找相关要求。至少我觉得在这一点上JML规格还是相当到位的,我从来没有遇到过读完了规格还不明白的特殊情况。

这么看来规格其实是一个保证方法设计者和方法实现者和平相处的妙药。实现者再也不会因为不知道特殊情况如何处理而大骂设计不说清楚,设计者也不需要再为实现者的错误理解买单了,毕竟规格都写好了,没理解是你的问题嘛。

这真的好耶!

posted @ 2019-05-21 17:30 ChenL 阅读(...) 评论(...) 编辑 收藏