(一)梳理JML语言的理论基础、应用工具链情况
梳理JML语言的理论基础
在JML官网上,是这样定义JML的。
Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为 。它结合了Eiffel的契约方法设计 和Larch系列接口规范语言的基于模型的规范方法 ,以及细化演算一些元素 。
基础语法梳理
https://blog.csdn.net/piaopu0120/article/details/89527175
链接为我根据预习资料和第一次上课内容进行的JML语法整理,内容较多,仍放在另一个博客里。
2.应用工具链
- Open JML:OpenJML是Java程序的程序验证工具,允许检查Java Modeling Language中注释的程序的规范;
- JML SMT Solver:OpenJML的主流SMT Solver;
- JML Unit:单元测试工具;
- JMLdoc:JML规范的javadoc(jmldoc)增强版本;
- jmlc:断言检查编译器。
(二)部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例, 并结合规格对生成的测试用例和数据进行简要分析
这个东西玄之又玄,成功的运行除了环境搭好外,还需要保证代码和逻辑的双重匹配。总之,感谢郑文帝对我的帮助。
以下是针对MyGraph接口生成的测试用例:
Failed: racEnabled() Passed: constructor MyGraph() Passed: <<MyGraph@4ca8195f>>.addPath(null) Passed: <<MyGraph@490d6c15>>.containsEdge(-2147483648, -2147483648) Passed: <<MyGraph@449b2d27>>.containsEdge(0, -2147483648) Passed: <<MyGraph@5479e3f>>.containsEdge(2147483647, -2147483648) Passed: <<MyGraph@27082746>>.containsEdge(-2147483648, 0) Passed: <<MyGraph@66133adc>>.containsEdge(0, 0) Passed: <<MyGraph@7bfcd12c>>.containsEdge(2147483647, 0) Passed: <<MyGraph@42f30e0a>>.containsEdge(-2147483648, 2147483647) Passed: <<MyGraph@24273305>>.containsEdge(0, 2147483647) Passed: <<MyGraph@5b1d2887>>.containsEdge(2147483647, 2147483647) Passed: <<MyGraph@46f5f779>>.containsNode(-2147483648) Passed: <<MyGraph@1c2c22f3>>.containsNode(0) Passed: <<MyGraph@33e5ccce>>.containsNode(2147483647) Passed: <<MyGraph@5a42bbf4>>.containsPathId(-2147483648) Passed: <<MyGraph@270421f5>>.containsPathId(0) Passed: <<MyGraph@52d455b8>>.containsPathId(2147483647) Passed: <<MyGraph@4f4a7090>>.containsPath(null) Passed: <<MyGraph@18ef96>>.getDistinctNodeCount() Failed: <<MyGraph@6f79caec>>.getPathById(-2147483648) Failed: <<MyGraph@67117f44>>.getPathById(0) Failed: <<MyGraph@5d3411d>>.getPathById(2147483647) Failed: <<MyGraph@2471cca7>>.getPathId(null) Passed: <<MyGraph@5fe5c6f>>.getShortestPathLength(-2147483648, -2147483648) Passed: <<MyGraph@6979e8cb>>.getShortestPathLength(0, -2147483648) Passed: <<MyGraph@763d9750>>.getShortestPathLength(2147483647, -2147483648) Passed: <<MyGraph@5c0369c4>>.getShortestPathLength(-2147483648, 0) Passed: <<MyGraph@2be94b0f>>.getShortestPathLength(0, 0) Passed: <<MyGraph@d70c109>>.getShortestPathLength(2147483647, 0) Passed: <<MyGraph@17ed40e0>>.getShortestPathLength(-2147483648, 2147483647) Passed: <<MyGraph@50675690>>.getShortestPathLength(0, 2147483647) Passed: <<MyGraph@31b7dea0>>.getShortestPathLength(2147483647, 2147483647) Passed: <<MyGraph@3ac42916>>.isConnected(-2147483648, -2147483648) Passed: <<MyGraph@47d384ee>>.isConnected(0, -2147483648) Passed: <<MyGraph@2d6a9952>>.isConnected(2147483647, -2147483648) Passed: <<MyGraph@22a71081>>.isConnected(-2147483648, 0) Passed: <<MyGraph@3930015a>>.isConnected(0, 0) Passed: <<MyGraph@629f0666>>.isConnected(2147483647, 0) Passed: <<MyGraph@1bc6a36e>>.isConnected(-2147483648, 2147483647) Passed: <<MyGraph@1ff8b8f>>.isConnected(0, 2147483647) Passed: <<MyGraph@387c703b>>.isConnected(2147483647, 2147483647) Failed: <<MyGraph@224aed64>>.removePathById(-2147483648) Failed: <<MyGraph@c39f790>>.removePathById(0) Failed: <<MyGraph@71e7a66b>>.removePathById(2147483647) Failed: <<MyGraph@2ac1fdc4>>.removePath(null) Passed: <<MyGraph@5f150435>>.size() =============================================== Command line suite Total tests run: 47, Failures: 9, Skips: 0 ===============================================
测试用例分析:
对于int类型的数据,生成的大多数是0,边界的正负数据;
对于object类型的数据,生成的大多是null。
对于参数是object类型的数据,应该就是挺难解决的。因为自动测试样例不太好生成满足要求的新对象。但是对于int类型全是边界数据,我有点难以理解。
(三)按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构
1.第九次作业
没有做太多架构上的设计,仅完成了目标函数的补充。在细节方面的小优化是用hashset进行去重保存节点;用hashmap实现id和Path的双重查找。
2.第十次作业
依然没有做架构上的设计,新的图没有选择继承之前图,导致MyGraph很冗杂。主要实现了对于每一个Path的每一个小邻边的访存;修改了add和remove,完善功能;为求最短路,进行了floyd的初始化和实现。
3.第十一次作业
第三次作业因为有四个相似的图,所以进行了架构上的优化。
虽然之前有考虑过指导书推荐的组合模式+工厂模式的架构,但当时主观觉得组合模式的树形结构不太好运用到这次的题(实际上是我建模的主体不对)。所以考虑了建造者模式。
建造者模式(Builder Pattern)
使用多个简单的对象一步一步构建成一个复杂的对象。
主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
用大白话讲就是生成一个套餐。
我生成了一个floyd套餐builder和一个dij套餐builder,这两个builder可以生成各种各样的套餐。
分析:
①把问题分成由floyd算法求解更优和dijkstra算法求解更优的两类
②把floyd和dijkstra的初始化与实现拆分成小块。
③将小块进行重新组合,实现builder。
顶层组合实现举例:
1 public Dij lessTransfer() { 2 if (transDij == null) { 3 transDij = new Dij(); 4 transDij.addStratege(new InitLessTransfer()); 5 transDij.addStratege(new NoneTransfer()); 6 } 7 return transDij; 8 } 9 10 public Dij lessCost() { 11 if (costDij == null) { 12 costDij = new Dij(); 13 costDij.addStratege(new InitLessMoney()); 14 costDij.addStratege(new Transfer2()); 15 } 16 return costDij; 17 } 18 19 public Dij lessUnplefation() { 20 if (unpleDij == null) { 21 unpleDij = new Dij(); 22 unpleDij.addStratege(new InitUnstatisfication()); 23 unpleDij.addStratege(new Transfer32()); 24 } 25 return unpleDij; 26 }
调用builder实例:
1 if (unpleFlag) { 2 // 省略初始化代码 3 // 如果没有缓存,调用builder1 4 unpleDij = DijBuilder.getBuilder().lessUnplefation(); 5 } else { 6 if (alreadyUnple.contains(fromNodeId)) { 7 // 省略结果返回代码 8 } 9 // 如果有缓存,调用builder2 10 unpleDij = DijBuilder.getBuilder().noneunpleDij(); 11 } 12 // 省略更新缓存代码 13 // 启动builder,得到结果 14 unpleDij.runDij(fromNodeId, unpleGraph, unpleExist, distict, 15 unpleNodeSort, buddy, pidMap, pathInformation); 16 }
下面是builder的具体实现:
以不满意度为例
DijBuilder
1 public class DijBuilder { 2 private Dij unpleDij = null; 3 private Dij noneunpleDij = null; 4 5 private static DijBuilder builder = new DijBuilder(); 6 7 private DijBuilder() { 8 } 9 10 public static DijBuilder getBuilder() { 11 return builder; 12 } 13 14 public Dij lessUnplefation() { 15 if (unpleDij == null) { 16 unpleDij = new Dij(); 17 unpleDij.addStratege(new InitUnstatisfication()); 18 unpleDij.addStratege(new Transfer32()); 19 } 20 return unpleDij; 21 } 22 23 public Dij noneunpleDij() { 24 if (noneunpleDij == null) { 25 noneunpleDij = new Dij(); 26 noneunpleDij.addStratege(new NoneInit()); 27 noneunpleDij.addStratege(new Transfer32()); 28 } 29 return noneunpleDij; 30 } 31 32 }
Dij
public class Dij { private List<Strategy> strategies = new ArrayList<>(); public void addStratege(Strategy s) { strategies.add(s); } public void runDij(int v0, int[][] distanceGraph, boolean[][] exist, HashSet<Integer> distict, HashMap<Integer, Integer> nodeSort, HashMap<String, Integer> buddy, HashMap<Path, Integer> pidMap, PathMap[] pathInformation) { Strategy initMethod = strategies.get(0); initMethod.process(0, distanceGraph, exist, distict, nodeSort, buddy, pidMap, pathInformation); Strategy calMethod = strategies.get(1); calMethod.process(nodeSort.get(v0), distanceGraph, exist, distict, nodeSort, buddy, pidMap, pathInformation); } }
Strategy接口
1 public interface Strategy { 2 void process(int v0, int[][] distanceGraph, boolean[][] exist, 3 HashSet<Integer> distict, 4 HashMap<Integer, Integer> nodeSort, 5 HashMap<String, Integer> buddy, 6 HashMap<Path, Integer> pidMap, PathMap[] pathInformation); 7 }
GenerateWay抽象类实现Strategy接口,Initial抽象类类似
1 public abstract class GenerateWay implements Strategy { 2 3 public abstract void process(int v0, int[][] distanceGraph, 4 boolean[][] exist, 5 HashSet<Integer> distict, 6 HashMap<Integer, Integer> nodeSort, 7 HashMap<String, Integer> buddy, 8 HashMap<Path, Integer> pidMap, 9 PathMap[] pathInformation); 10 }
Transfer32类继承抽象类GenerateWay
1 public class Transfer32 extends GenerateWay { 2 3 public void process(int v0, int[][] distanceGraph, boolean[][] exist, 4 HashSet<Integer> distict, 5 HashMap<Integer, Integer> nodeSort, 6 HashMap<String, Integer> buddy, 7 HashMap<Path, Integer> pidMap, 8 PathMap[] pathInformation) { 9 StaticFun.dij(v0, distict.size(), 32, distanceGraph, exist); 10 } 11 }
这样的方法使得在顶层实现不同的floyd或者dij的算法非常容易,可以实现n*m种任意的组合,并且不需要改动任何的底层代码。
但在实现的过程中也会遇到一些困难:
1. 统一接口导致传参过多;
2. 没有办法在顶层进一步封装,使得顶层代码重复变多。
(四)按照作业分析代码实现的bug和修复情况
1. 第九次作业
第九次作业被检测出来了一个bug,compareTO用了一种错误的方法,当时为了少些几行所以是通过差值判断的。这直接导致了溢出时判断错误。修复的时候把它修改成了大小比较。
2. 第十次作业
在判断最短路的时候忘记了两个相同点的情况,修复的时候把这个条件加上了。
3. 第十一次作业
感谢莫策同学的数据生成器,经过很多数据的互拍让bug数减到最小。
总结
这些bug很不值得,他、它们都是很基础很容易用测试用例覆盖的问题。但这两次作业我都没怎么用心测试程序,导致了这两个bug的出现。第十一次作业我汲取了之前的教训,进行了大量的测试使得最后一次作业在强测互测都没有发现bug。以后一定谨记,在每一次测试之前优先考虑边界问题和特殊数据。
(五)阐述对规格撰写和理解上的心得体会
- 这几次作业中,在课上我们有机会写规格,课下我们主要是通过阅读规格。但是实际上,我在课下补充一个方法时,会不根据规格来撰写代码,只是会根据规格来作为debug的一层手段。我记得在第一次作业我准备按照规格来写代码的时候,发现使用的架构和数据结构与规格表述的不太适配。
- 在后期阅读规格的时候,比如像计算容器类的不同节点个数或者最少换乘数路径这样的题目,用语言来描述很简单,但是它的逻辑用规格来书写就非常困难。写规格的感觉对于我来说特别像写离散数学证明题,需要用非常完备的逻辑来完善我们所想表达的。
- 一个正确的规格有助于减少歧义,规格对于方法目的的说明非常有效,因为它是贴合数据代码的,挺像伪代码对于一个问题的说明功效。
- 规格描述了数据的变与不变,忽略了中间过程的实现,是结果指向的。
后话:
这个单元的学习让我觉得挺迷的,因为JML的资料很少,对我来说环境安装和实际使用的可行性很低。虽然有同学能够很好的使用,但是有些时候这种成功不是所有人都能复刻的,迎接我们的可能还是一大堆不明所以的报错。这个单元的编程经历还是带给我了一些收获:
1.架构上的设计
这个单元对于架构上的设计思考得很多,每一个单元写优化的时候最先考虑的是优化架构。这个单元涉及到的图的访存还蛮复杂的,因为一直避免疯狂嵌套hashmap,所以想了很多办法。总的来说收获很多吧。
2.规范性
规格带给我的体验不是很多,但是能够从规格感受到代码规范的重要性。而且相较于前几次从头开始写,这几次主要是补充方法,由于父类接口的限制使写代码的时候要更好的设计。
这个单元的第一次作业也是我第一次翻车,会好好反思的。