——“基于正确规格的程序就可以被认为是正确的程序 ”
本单元的三次作为均为根据jml规格完成代码。三次作业依次为实现Path路径类和PathContainer路径容器类;将PathContainer类扩展成Graph类以实现图相关问题的处理;将Graph类扩展成RailwaySystem类以支持多种最短路查询(完成数据结构图论专题练习)。
本文将从JML语言梳理、部署SMT Solver进行验证、部署JML UnitNG并针对Graph接口的实现进行测试、梳理架构设计以及分析代码实现的bug和修复情况方面对第三单元的三次作业进行总结,并在最后阐述对规格撰写和理解上的心得体会。
JML语言梳理
理论基础
面向对象编程思想中有一个重要原则就是尽量推迟过程性的思考。所以我们在设计面向对象程序时首先要考虑的要干什么而不是怎么干什么。而Java本身并没有提供一种描述程序要干什么的方法,于是我们引入Java建模语言JML(Java Modeling Language)来实现这一目的。
JML是一种行为接口的规范语言,可以用来指定Java模块的行为,其最基本的用途是作为Java的合同设计(DBC)语言。
JML在实际设计中主要有两种用途:
在实现前对设计的规格进行精确传达
JML语言通过对规格进行精确表述,使架构人员和代码实现人员达成一致,可以认为是程序猿之间进行交流的猿语。
对已实现的代码进行总结梳理
这方面的作用主要体现在提高代码的可维护性。
实际上,如果我是维护人员,可能我只会在第一次通读规格,然后用自然语言进行注释
(毕竟还是要说人话的)。这种感觉有点像把精妙但晦涩文言文翻译成粗糙易懂白话文,白话文有什么地方解释得模糊了,再去查原文。
应用工具链
首先通过开源的JML编译器,比如OpenJml,编译含有JML标记的代码。
对于Openjml来说
-check
选项可以对生成的类文件进行JML规范检查。Openjml中还包含z3等SMT Solver,可以对代码等价性进行验证。通过Openjml
-esc
选项对代码的静态验证是不依赖JML的,SMT Solver会自动整理JML(当然存在整理不出来的情况,这时就只能手动添加一波,强烈建议统一源码和jar包)。JML UnitNG可以生成一个Java类文件测试的框架,基于JML并结合Openjml的
-rac
运行时检查选项,实现对代码的自动化测试。据观察,这种测试主要是边界测试。JMLdoc工具与Javadoc工具类似,可在生成的HTML格式文档中包含JML规范。
部署SMT Solver进行验证
SMT(Staisfiability modulo theories) Solver即定理证明器。
通过OpenJML下载OpenJML,然后根据讨论区霸霸的OpenJML基本使用部署OpenJML的同时就能拥有SMT Solver啦~
其工作机理是:
OpenJML将JML规范转换为SMT-LIB格式,并将Java+JML程序所隐含的证明问题传递给后端SMT求解器。
OpenJML支持主要的SMT解决方案,如Z3、CVC4和YIES。
证明的成功将取决于SMT解算器的能力(例如,它支持哪些逻辑)、代码+规范的特定逻辑编码以及代码和规范编写的复杂性和样式。
使用命令
java -jar <path>/openjml.jar -esc <soursefile> # 未按上述链接部署 openjml -esc <sourcefile> # 按上述链接添加了openjml脚本
即可对代码进行静态检查。
下文是使用SMT Solver对第一次作业中各方法进行验证的结果。
public MyPath(int... nodeList)
// 1.下标可能为负 homework/MyPath.java:16: warning: The prover cannot establish an assertion (PossiblyNegativeIndex) in method MyPath this.nodeList.add(nodeList[i]); ^ /* // 修改前代码 public MyPath(int... nodeList) { this.nodeList = new ArrayList<>(); for (int i = 0; i < nodeList.length; i++) { this.nodeList.add(nodeList[i]); } } */ // 修改后代码 public MyPath(int... nodeList) { this.nodeList = new ArrayList<>(); for (int i = 0; i < nodeList.length; i++) { if (i >= 0) { this.nodeList.add(nodeList[i]); } } }
public int compareTo(Path o)
/usr/share/java/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):71: warning: The prover cannot establish an assertion (Invariant) in method compareTo //-RAC@ public invariant content.theSize >= 0; ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):70: warning: The prover cannot establish an assertion (Invariant) in method compareTo //-RAC@ public invariant content.owner == this; ^
public boolean equals(Obj o)
homework/MyPath.java:45: warning: The prover cannot establish an assertion (ExceptionalPostcondition: /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):76: ) in method equals if (!(this.size() == ((Path) varl).size())) { ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):76: warning: Associated declaration: homework/MyPath.java:45: @ public normal_behavior ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):70: warning: The prover cannot establish an assertion (Invariant) in method equals //-RAC@ public invariant content.owner == this; ^ homework/MyPath.java:50: warning: The prover cannot establish an assertion (Postcondition: /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):78: ) in method equals return false; ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):78: warning: Associated declaration: homework/MyPath.java:50: @ ensures \result; ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/util/Collection.jml):71: warning: The prover cannot establish an assertion (Invariant) in method equals //-RAC@ public invariant content.theSize >= 0; ^ /* // 修改前代码 public boolean equals(Object varl) { if (varl == null || !(varl instanceof Path)) { return false; } if (!(this.size() == ((Path) varl).size())) { return false; } for (int i = 0; i < this.size(); i++) { if (!(this.getNode(i) == ((Path) varl).getNode(i))) { return false; } } return true; } */ // 修改后代码 public boolean equals(Object varl) { if (varl == null || !(varl instanceof Path)) { return false; } int thisSize; int varlSize; try { thisSize = this.size(); } catch (Exception e) { return false; } try { varlSize = ((Path) varl).size(); } catch (Exception e) { return false; } if (thisSize != varlSize) { return false; } int i; for (i = 0; i < thisSize; i++) { try { if (!(this.getNode(i) == ((Path) varl).getNode(i))) { break; } } catch (Exception e) { return false; } } if (i < thisSize) { return true; } else { return false; } }
public int hashCode()
homework/MyPath.java:87: warning: The prover cannot establish an assertion (Postcondition: /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):63: ) in method hashCode return nodeList.hashCode(); ^ /usr/share/java/openjml-0.8.42/openjml.jar(specs/java/lang/Object.jml):63: warning: Associated declaration: homework/MyPath.java:87: //-RAC@ ensures \result == theHashCode; ^
可见,静态验证可能更适合于更大型的项目。在我们的作业中应用使我们的代码中需要额外考虑一些可能并不会出现的exception,导致代码不够简明。
部署JMLUnitNG并针对Graph接口的实现进行测试
JMLUnitNG是JMLUnit的一个基于TestNG的继承者,而TestNG(Test Next Gerneration)是一个开源自动化测试框架。
JMLUnitNG的部署方法还是仰仗同一个霸霸使用 JMLUnitNG 生成 TestNG 测试样例。
以下是测试结果:
[TestNG] Running: Command line suite Passed: racEnabled() Passed: constructor MyGraph() Passed: <<homework.MyGraph@18ef96>>.addPath(null) Passed: <<homework.MyGraph@ba4d54>>.containsEdge(-2147483648, -2147483648) Passed: <<homework.MyGraph@de0a01f>>.containsEdge(0, -2147483648) Passed: <<homework.MyGraph@4c75cab9>>.containsEdge(2147483647, -2147483648) Passed: <<homework.MyGraph@1ef7fe8e>>.containsEdge(-2147483648, 0) Passed: <<homework.MyGraph@6f79caec>>.containsEdge(0, 0) Passed: <<homework.MyGraph@67117f44>>.containsEdge(2147483647, 0) Passed: <<homework.MyGraph@5d3411d>>.containsEdge(-2147483648, 2147483647) Passed: <<homework.MyGraph@2471cca7>>.containsEdge(0, 2147483647) Passed: <<homework.MyGraph@5fe5c6f>>.containsEdge(2147483647, 2147483647) Passed: <<homework.MyGraph@6979e8cb>>.containsNode(-2147483648) Passed: <<homework.MyGraph@763d9750>>.containsNode(0) Passed: <<homework.MyGraph@2be94b0f>>.containsNode(2147483647) Passed: <<homework.MyGraph@d70c109>>.containsPathId(-2147483648) Passed: <<homework.MyGraph@17ed40e0>>.containsPathId(0) Passed: <<homework.MyGraph@50675690>>.containsPathId(2147483647) Skipped: <<homework.MyGraph@31b7dea0>>.containsPath(null) Passed: <<homework.MyGraph@3ac42916>>.getDist() Passed: <<homework.MyGraph@47d384ee>>.getDistinctNodeCount() Passed: <<homework.MyGraph@2d6a9952>>.getGraph() Failed: <<homework.MyGraph@22a71081>>.getPathById(-2147483648) Failed: <<homework.MyGraph@3930015a>>.getPathById(0) Failed: <<homework.MyGraph@629f0666>>.getPathById(2147483647) Failed: <<homework.MyGraph@1bc6a36e>>.getPathId(null) Failed: <<homework.MyGraph@1ff8b8f>>.getShortestPathLength(-2147483648, -2147483648) Failed: <<homework.MyGraph@387c703b>>.getShortestPathLength(0, -2147483648) Failed: <<homework.MyGraph@224aed64>>.getShortestPathLength(2147483647, -2147483648) Failed: <<homework.MyGraph@c39f790>>.getShortestPathLength(-2147483648, 0) Failed: <<homework.MyGraph@71e7a66b>>.getShortestPathLength(0, 0) Failed: <<homework.MyGraph@2ac1fdc4>>.getShortestPathLength(2147483647, 0) Failed: <<homework.MyGraph@5f150435>>.getShortestPathLength(-2147483648, 2147483647) Failed: <<homework.MyGraph@1c53fd30>>.getShortestPathLength(0, 2147483647) Failed: <<homework.MyGraph@50cbc42f>>.getShortestPathLength(2147483647, 2147483647) Passed: <<homework.MyGraph@75412c2f>>.getUpdateDist() Failed: <<homework.MyGraph@282ba1e>>.isConnected(-2147483648, -2147483648) Failed: <<homework.MyGraph@13b6d03>>.isConnected(0, -2147483648) Failed: <<homework.MyGraph@f5f2bb7>>.isConnected(2147483647, -2147483648) Failed: <<homework.MyGraph@73035e27>>.isConnected(-2147483648, 0) Failed: <<homework.MyGraph@64c64813>>.isConnected(0, 0) Failed: <<homework.MyGraph@3ecf72fd>>.isConnected(2147483647, 0) Failed: <<homework.MyGraph@483bf400>>.isConnected(-2147483648, 2147483647) Failed: <<homework.MyGraph@21a06946>>.isConnected(0, 2147483647) Failed: <<homework.MyGraph@77f03bb1>>.isConnected(2147483647, 2147483647) Failed: <<homework.MyGraph@326de728>>.removePathById(-2147483648) Failed: <<homework.MyGraph@25618e91>>.removePathById(0) Failed: <<homework.MyGraph@7a92922>>.removePathById(2147483647) Failed: <<homework.MyGraph@71f2a7d5>>.removePath(null) Passed: <<homework.MyGraph@2cfb4a64>>.size() =============================================== Command line suite Total tests run: 50, Failures: 26, Skips: 1 ===============================================
这些测试数据中,如果方法声明中变量是int类型的话,会对int范围内的极大极小值和0进行测试;如果是Integer类型还会额外增加对空指针null的测试。
对于通过JMLUnitNG生成测试样例我还有许多不解,希望有dalao能够救救孩子。
首先,可以看到结果中有许多failed,看起来非常不详。
我的第一个疑惑是failed的含义是什么?是指我的程序运行结果与预期不符,还是指我的程序没有正常执行完,比如抛出了异常?
其次,通过这种方式生成的测试是否与jml相关?
我试图通过实验验证,于是对较为简单的Path类进行了以上测试,结果如下:
[TestNG] Running: Command line suite Passed: racEnabled() Failed: constructor MyPath(null) Passed: constructor MyPath({}) Failed: <<homework.MyPath@1>>.compareTo(null) Passed: <<homework.MyPath@1>>.containsNode(-2147483648) Passed: <<homework.MyPath@1>>.containsNode(0) Passed: <<homework.MyPath@1>>.containsNode(2147483647) Passed: <<homework.MyPath@1>>.equals(null) Passed: <<homework.MyPath@1>>.equals(java.lang.Object@1c2c22f3) Passed: <<homework.MyPath@1>>.getDistinctNodeCount() Failed: <<homework.MyPath@1>>.getNode(-2147483648) Failed: <<homework.MyPath@1>>.getNode(0) Failed: <<homework.MyPath@1>>.getNode(2147483647) Passed: <<homework.MyPath@1>>.hashCode() Passed: <<homework.MyPath@1>>.isValid() Passed: <<homework.MyPath@1>>.iterator() Passed: <<homework.MyPath@1>>.size() =============================================== Command line suite # Total tests run: 17, Failures: 5, Skips: 0 ===============================================
可以看到以上failed样例对应的方法都是有前置条件@requires的方法,如果该测试是依赖jml的,即生成的测试样例一定是符合jml的,且产生的结果是否正确也是以jml为依据的,那就应该在我添加了相关@requires限制之后就不存在failed样例了,然而我进行了以上操作之后结果并没有改变。
如果这意味着这种测试方法是jml无关的话,那是否passed显得也并不那么重要了,毕竟在不满足规格中的@requires条件的情况下结果是不可预测的,这就使TestNG的价值降低了不少,因为这样对于每一个failed样例都需要人工判断是否真的错误?
最后墙裂建议统一源码和jar包!!!
梳理架构设计
第一次作业就是粗暴的接口长啥样我长啥样。
第一次作业类图 第二次作业也基本上和接口长一样,不过可能第二次作业意识到规格只是对结果的限制,对实现并没有限制,于是写得随意了许多。另外,第二次作业中的Graph类实际上可以继承PathContainer类,但我脑子一热就顺手CtrlCV了,到第三次作业就发现,幸好当时脑子热了。
第二次作业类图 第三次作业的架构是阳春白雪和下里巴人的结合。之前的部分不想重构了,扩展的部分又继承了之前的部分,就非常,,,混搭。
第三次作业花式最短路,我选择了拆点算法,而且是菊花点。然后架构大概长这样:
其中NodeWithPathId继承了Pair类,是储存在图中的节点结构,包含了NodeId和它所属的PathId;GraphStructure是抽象的图类,包含已实现的加路径、删路径和计算实际最短路结果的方法,以及未实现的加边、删边和求最短路方法;
TicketPriceGraph(TPG)类和抽象类MultiPointsWeightGraph(MPWG)类均继承了GraphStruceture类,其中TPG中最短路的实现方法为BFS,MPWG是带权图抽象类,最短路的实现方法为堆优化Dijstra,其中还声明了未实现的求权重方法;
最后TransferGraph和Unpleasant类均继承MPWG类,各自实现不同的求权重方法。
实际上如果将第二次作业的架构也整合进去的话,可以直接将TicketPriceGraph类作为无权图类,然后TicketPriceGraph和ShortestPathLength各自作为该类实例化出的对象即可。
同时,如果使用泛型还可以将有权图和无权图的方法进一步复用,然而知识水平暂时不资瓷,就先这样了。
第三次作业类图
分析代码实现的bug和修复情况
这一部分的题目看起开非常有趣,”分析代码实现的bug“,我的代码确实实现了一堆bug QAQ
第一次作业
第一次作业因为还不了解jml只是对结果的约束,以为只能有两个线性存储结构,导致DISTINCT_NODE_COUNT方法时间复杂度过高,出现了TLE的问题。
通过增加一个HashMap存储当前某个节点出现了多少次,查询时输出size()解决了该问题。
第三次作业
墨菲定律车祸现场,以为自己稳了,结局确实稳C了。
首先,come some music,如果我不会写dij你会爱我吗~
原来的写法存在部分节点realDist更新后没有重新进入优先队列的问题(强测这分已经是撞大运了
@Override public void shortestPath(Integer st, HashMap<Integer, Integer> dist, HashSet<Integer> pathIdContainsSt) { HashMap<NodeWithPathId, Integer> realDist = new HashMap<>(); PriorityQueue<NodeWithPathId> queue = new PriorityQueue<>(new Comparator<NodeWithPathId>() { @Override public int compare(NodeWithPathId o1, NodeWithPathId o2) { return realDist.get(o1).compareTo( realDist.get(o2)); } }); NodeWithPathId uu = new NodeWithPathId(st, terminalPathId); realDist.put(uu, new Integer(0)); queue.offer(uu); while (!queue.isEmpty()) { uu = queue.poll(); for (Pair<NodeWithPathId, Integer> vvWithValue : this.get(uu).keySet()) { NodeWithPathId vv = vvWithValue.getKey(); Integer ww = vvWithValue.getValue(); Integer newDist = new Integer(realDist.get(uu) + ww); /* 它原来长这样 if (!realDist.containsKey(vv)) { realDist.put(vv, newDist); queue.offer(vv); } else if(newDist < realDist.get(vv)) { // 更新后没有入队 realDist.put(vv, newDist); } */ if (!realDist.containsKey(vv) || (realDist.containsKey(vv) && newDist.compareTo(realDist.get(vv)) < 0)) { realDist.put(vv, newDist); if (!queue.contains(vv)) { queue.offer(vv); } } } } calculateDist(dist, realDist); }
其次,由于第三次作业继承了第二次作业,但是对于图修改指令又有所扩充,脑子一抽就没检查扩充后的方法是否符合规格,导致addPath方法中,如果该边存在,还是会重新加一遍相同的边。
@Override public int addPath(Path varl) throws Exception { int lastId = getId(); int addPathId = super.addPath(varl); // 原来它长这样 // if (addPathId != 0) { if (addPathId > lastId) { ticketPriceGraph.addPath(addPathId, varl, termials); transferGraph.addPath(addPathId, varl, termials); unpleasentGraph.addPath(addPathId, varl, termials); addForTerminals(addPathId, varl); } return addPathId; }
对规格撰写和理解上的心得体会
对于规格撰写,有以下几点体会:
- 清楚规格只限定结果不规定实现的特性;
- 要熟悉语法,同时规格定义中只能调用pure方法;
- 可以先从数学的角度进行分析,对充分必要条件拆分成充分条件和必要条件进行分别表述。
对于规格的理解大概就是像读os代码一样,寻寻觅觅
(凄凄惨惨戚戚)追根溯源地通读一遍,有个大概轮廓,然后在写代码的过程中哪里模糊了再去查询细究之类。