一、
1.JML语言理论基础
JML,全称为Java Modeling Language,根据名字可以容易的知道JML是对Java程序进行规格化设计的一门语言。JML是一种行为接口规格语言,可用于指定Java模块的行为。它结合了Eiffel的契约方法设计和Larch系列接口规范语言的基于模型的规范方法,以及细化演算的一些元素。
通过对JML的阅读,我们可以了解一个方法的前提和作用结果,以及其产生的副作用。
示例:
1 /*@ normal_behavior 2 @ requires (\exists Path path; path.isValid() && containsPath(path); path.containsNode(fromNodeId)) && 3 @ (\exists Path path; path.isValid() && containsPath(path); path.containsNode(toNodeId)); 4 @ assignable \nothing; 5 @ ensures (fromNodeId != toNodeId) ==> \result == (\exists int[] npath; npath.length >= 2 && npath[0] == fromNodeId && npath[npath.length - 1] == toNodeId; 6 @ (\forall int i; 0 <= i && (i < npath.length - 1); containsEdge(npath[i], npath[i + 1]))); 7 @ ensures (fromNodeId == toNodeId) ==> \result == true; 8 @ also 9 @ exceptional_behavior 10 @ signals (NodeIdNotFoundException e) (\forall Path path; containsPath(path); !path.containsNode(fromNodeId)); 11 @ signals (NodeIdNotFoundException e) (\forall Path path; containsPath(path); !path.containsNode(toNodeId)); 12 @*/ 13 public boolean isConnected(int fromNodeId, int toNodeId) throws NodeIdNotFoundException;
normal_behavior是正常方法的作用;
exceptional_behavior是抛出异常;
requires是一些要求,在本例中,要求存在包含了fromNodeId、toNodeId的有效路径,否则抛出相应的异常
assignable是产生的副作用,在本例中无副作用(\nothing)
ensures是方法结束后满足的状态,在本例是判断了fromNodeId与toNodeId是否连通。
2.应用工具链
官方网站中,提供了断言检查编译器(jmlc)、单元测试工具(jmlunit),以及了解JML规范的javadoc(jmldoc)。实际使用中,我们常常使用OpenJML检查JML的规范性,使用JMLUnit或者JMLUnitNG生成数据测试代码。
二、JMLUnit测试
测试代码:
1 public class Atest { 2 public int add(int a, int b) { 3 return a + b; 4 } 5 public int sub(int a, int b) { 6 return a - b; 7 } 8 public int multi(int a, int b) { 9 return a * b; 10 } 11 public int div(int a, int b) { 12 return a / b; 13 } 14 public int and(int a, int b) { 15 return a & b; 16 } 17 public int or(int a, int b) { 18 return a | b; 19 } 20 }
测试结果:
三、架构设计
1.第一次作业:
第一次作业比较简单,只需要按照JML描述补充相应代码即可。
Path类中,利用ArrayList存储path的具体结点情况,利用HashSet存储path中存在的结点。
PathContainer类中,用一个int型变量产生path的id,每次加入一条路径,id++。用HashMap存储路径,key为path的id,value为path。用HashMap存储所有的结点,key为结点,value为该结点的数目。本次作业最容易产生复杂度的方法就是getDistinctCount。我采取的做法是在每次改变路径时,即增加或删去一条path时,调用相关的adddistinct(Path)、subdistinct(Path)方法及时更新存储结点的HashMap,如此该HashMap的元素的数量便是所有不同结点的数量。
2.第二次作业:
本次作业在第一次作业的基础上新加入AdjacencyList类,实现邻接表,在每次改变图结构时通过addadjacency、subadjacency方法更新邻接表。
AdjacencyList类:用一个HashMap实现邻接表,key为结点,value为存储该结点的邻接点的ArrayList,注意,ArrayList中可以出现相同的元素,因为一个结点可以通过不同的路径而与另一个结点邻接。add与remove方法用于更新邻接表。通过bfs实现了判断两结点是否连通的方法isConnected以及求两点的最短路径的shortestLength。
Graph类中只需调用其他类中已经实现的相关方法即完成目标。
3.第三次作业:
在本次作业中,相比第二次作业,主要是增加了带权值的图的最短“路径”。思路也很暴力,每次更新图时,通过Floyd更新最短路径、最低不满意度。
最低价格:先将所有不需要换乘的情况加入,再通过Floyd进行更新,每次对某个值进行更新相当于进行了一次换乘,则加上换乘的花费即可。
最低换乘次数:利用bfs,每次更新获得这一次换乘能到的所有站,直到找到目的地站即可。
最低不满意度:与最低价格类似。
联通块数目:每次改变路径时,更新联通块,联通块以一个HashMap存储,key为结点所在的块的序号,value为结点。每次更新时,先判断这条路径中是否存在已经知道块号的结点,已知则把所有结点放到该块中,没有则新建一个块(增加块号)。
四、bug以及修复情况
- 第一次作业:强测中CPU TIME爆炸,主要是distinctcount方法复杂度太高,修复后,引入HashMap存储某个结点的数目,并将复杂度分散到每次改变图结构时。例如,在addPath时,增加结点数目。
1 public int addPath(Path path) { 2 if (path != null && path.isValid()) { 3 if (!this.containsPath(path)) { 4 this.pathHashMap.put(id, path); 5 id++; 6 this.adddistinct(path); 7 return id - 1; 8 } else { 9 try { 10 return this.getPathId(path); 11 } catch (PathNotFoundException e) { 12 return 0; 13 } 14 } 15 } else { 16 return 0; 17 } 18 }
第6行即this.adddistinct(path)可实现目的,具体方法代码如下。
1 private void adddistinct(Path path) { 2 for (Iterator nodes = path.iterator(); nodes.hasNext();) { 3 int node = (Integer) nodes.next(); 4 if (this.nodeamount.containsKey(node)) { 5 this.nodeamount.replace(node, this.nodeamount.get(node) + 1); 6 } else { 7 this.nodeamount.put(node, 1); 8 } 9 } 10 }
- 第一次作业:强测中CPU TIME爆炸,主要是distinctcount方法复杂度太高,修复后,引入HashMap存储某个结点的数目,并将复杂度分散到每次改变图结构时。例如,在addPath时,增加结点数目。
- 第二次作业:未发现bug。
- 第三次作业:依然是CPU TIME爆炸,据我分析是Floyd的原因,尤其是求最低价格和最低不满意度时,修复需要引入新方法来实现相关函数,借鉴讨论区中提出的拆点的方法。
五、心得体会
本次作业,我第一次接触了规格以及JML,同时经历了配置OpenJML的痛苦。我认为,一定要多与其他人沟通,对于其他人的方法进行思考,是否比自己的复杂度更低。本次作业,我的面向对象思想更进一步,不再像以前的作业次次重构,本单元也是由于作业的特点,我重构的部分并不是很多,大多数是在原来基础上增添一点点方法即可。JML作为规格语言,在描述程序功能方面的作用很大,掌握好规格,更有利于完善自己的架构能力。总体来说,我认为本单元我的收获非常多,进步也很大,要继续保持。