面向对象学习的第三个单元是让我们熟悉JML这一种格式注释/设计层面伪代码,然后并进行实战使用。
第一次作业是根据接口中的JML注释自己完成一个类调用接口,内容还是相对简单的,但是要考虑时间复杂度。
闲聊一下需求和数据结构:一共有两个类path和pathcontainer。(方便大家回想,我大致重新将需求罗列了一下,严格定义可以看指导书)
1 基础函数补充 2 //将nodeList数组存入该类中,(nodelist中的数可以重复) 3 构造函数:public Path(int[] nodeList) 4 //完成path迭代器的构造 5 Iterator<Integer> iterator() 6 //完成字典顺序排大小 7 int compareTo(Path o) 8 9 接口函数补充: 10 public interface Path extends Iterable<Integer>, 11 Comparable<Path> { 12 //nodelist的个数,不计重复 13 int size(); 14 //根据索引获得对应的node的值 15 int getNode(int index); 16 //判断是否存在指定的node 17 boolean containsNode(int nodeId); 18 //计算node不同的个数 19 int getDistinctNodeCount(); 20 //判断提供的path和该类是否等价(只要node顺序和数量一致就算相等) 21 boolean equals(Object obj); 22 //判断path是否有效,nodelist的长度超过2即可 23 boolean isValid(); 24 }
那么,这一块并没有太多的改进点,用arraylist或者数组都可以,但是数组会比较麻烦,因为需要考虑扩容的问题,虽然指导书中有说到长度最多不会超过1000,但也最好不要一上来就开个1000的数组。(当然,如果你说再提供一个hashmap来索引node来提高containnode的速度,因为有长度限制,所以我觉得不是那么紧迫)
对于计算不同的点的个数方面,基于path的node是不可以修改的,所以在构造函数时,可以采用set的结构提前将个数计算出来。
1 基础函数: 2 构造函数:public PathContainer() 3 4 接口函数: 5 public interface PathContainer { 6 //计算这个类里面存了多少path 7 int size(); 8 //查看类中是否有提供path的这条路径,只要相等就算一样 9 boolean containsPath(Path path); 10 //查看类中是否有提供id的path 11 boolean containsPathId(int pathId); 12 //通过id获得对应的path,没有就抛错误 13 Path getPathById(int pathId) throws PathIdNotFoundException; 14 //通过path获得对应的id,异常就抛错误 15 int getPathId(Path path) throws PathNotFoundException; 16 //增加一条路径到这个类里面,同时返回生成的与这条路径相对应的id 17 int addPath(Path path); 18 //移出某一条路径,出错抛异常 19 int removePath(Path path) throws PathNotFoundException; 20 //通过id移出某一条路径,出错抛异常 21 boolean removePathById(int pathId) throws PathIdNotFoundException; 22 //获得所有path中的node中不同的个数 23 int getDistinctNodeCount(); 24 }
我认为计算不同点的个数,如果这个函数要要降低复杂度的话,在增加和删除的时候会增加复杂度,这里暂时不考虑。使用arraylist,hashmap(path2id)和反向hashmap((id2path),双向hashmap(path2id和id2path)在各个函数中的复杂度度。
可能大家会有一个疑问,为什么在双向hashmap在removepath和removepathid变成了O(1)而不是hashmap和反向hashmap的最大值O(n)。我们注意到两个函数中,两者各有一个是O(1)也就是说,无论我们提供id或者path,总会有一个hashmap能够在O(1)的时间内获得path和id,那么另外一个hashmap可以用O(1)的时间用path或者id进行查找并删除。
by the way:默认的hashcode是根据对象的地址做一个映射得出的。所以内容相同的path,只要他们地址不一样,他们就会产生不同的hashcode。所以,根据我们的需求(内容相同的path属于一个path)应该将path的hashcode进行重写,因为,输入是一个数组int,输出是一个int,所以在处理的过程中需要时刻提防溢出的问题。这里,我才用的hashcode计算依据一下迭代式:hashcode = (hashcode * 307 + a[i] % 1024) %2048 (使用307是因为307是质数,使用1024和2048 是为了方便编译器简化计算)
第二次作业是建立在原先的基础上的补充,这次增加了一个Graph的子类(继承PathContainer)。需要我们完成一些关于图算法的内容。
1 public interface Graph extends PathContainer { 2 //是否包含某个点 3 boolean containsNode(int nodeId); 4 //是否存在from-to的一条边 5 boolean containsEdge(int fromNodeId, int toNodeId); 6 //from和to是否联通 7 boolean isConnected(int fromNodeId, int toNodeId) throws NodeIdNotFoundException; 8 //from和to的最短路径 9 int getShortestPathLength(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException; 10 }
鉴于每次要计算图的时候需要点集和边集都需要计算好,所以我决定在增加和减少path的时候对这两个集合进行维护、这一次的点集和边集我采用了hashmap的形式来表现每条边和点的索引次数,在增加path时往对应的index增加值或者增加对应的key-value键值对,在删除的时候进行对应的减少和移除。
关于图算法,考虑到运行的次数并不多所以我是用了floyd算法来计算。而计算是否联通时也是先更新了floyd的矩阵再判断是否联通。
第三次作业继续在原先的基础上加大难度,要求我们补充的是一个继承graph的子类(RailwaySystem)
1 public interface RailwaySystem extends Graph { 2 //获得最低的票价,边为1,换乘为2 3 int getLeastTicketPrice(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException; 4 //获得最低换乘数 5 int getLeastTransferCount(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException; 6 //获得最低不爽度 7 int getLeastUnpleasantValue(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException; 8 //获得最小联通块数 9 int getConnectedBlockCount(); 10 }
①联通块数:当我们拥有完整的最短路径矩阵之后这件事情就变得非常的简单。我们可以想象一下,这样的一个矩阵,通过相同的行变换和列变换可以变成一个对角块矩阵(对角块之外的数字都是无穷)大致流程如下
1 input: matrix_connect[][],不连通为-1 2 output:count 3 4 initial count as 0 5 put 1 to msize into Nodeset 6 msize = matrix_connect 7 for i from 1 to msize: 8 if i in Nodeset: 9 Nodeset.remove(i) 10 for node in Nodeset: 11 if matrix_connect[i][node]!=-1 12 Nodeset.remove(node) 13 count++ 14 return count
②带权最短路径:后面三个问题包括最少换乘数,最少票价和最低不爽度。这三个问题是很相似的。对于第一个问题来说,走边的开销是0,换乘的开销是1;对于第二个问题,走边的开销1,换乘开销2;对于第三个问题,走边的开销是正值,换乘的开销是32.(具体细节请参考指导书)。实际上这三个问题可以使用单源最短路径或者多源最短路径算法来解决。(具体算法请点链接,ps 多元最短路径那个博客网站做的贼漂亮)。但是,第一个问题可以使用一种更加高效的方法,利用path拥有所有的点的set,以及点对应所有path的set来快速解决问题。
1 输入:nodeid2pathset//通过nodeid找到所有通过该点的path的set 2 path2nodeidset//通过path找到属于该path的所有点的id的set 3 fromid,toid 4 输出:from-to的最小换乘数 5 6 init Nodeset 7 init oldNodeset 8 init count as 0 9 oldNodeset.add(fromid) 10 while(toid is not in oldNodeset ): 11 init newNodeset 12 for oldindex in oldNodeset: 13 for path in nodeid2pathset(index): 14 for newindex in path2nodeidset(path): 15 if (newindex not in Nodeset): 16 Nodest.add(newindex) 17 newNodeset.add(newindex) 18 oldNodeset = newNodeset 19 count++ 20 return count
当然,考虑到在计算时需要的数据的索引问题,所以还是要增加一些数据结构来增加运算的速度。
一、bug和错误反思:
在第一次作业中,我只拿到了一半的分数,问题主要集中在两个方面:①不理解让程序运行得快的主要方向是什么②在比较大小时,没有考虑整数有可能的溢出问题。
①这其实也是助教想让我们去理解的一个问题,在写程序时,将已经算好的东西保存下来是一件很重要的事情。
②在int比较大小时,尽量使用大于号小于号,或者使用Integer.compare这个方法。这里多聊一句,Integer这个类在比较时不能用‘==’,它的‘==’并不是比较大小的。这个方面还是尽量使用int来作为过程中变量类型。
在第二次作业中,相对比较顺利。
在第三次作业中,出现了一个相对隐蔽的bug,我和同学之间的所有互测都没有发现这个bug。问题出在我在计算时会覆盖前面的内容,但是前面的计算才是正确的,应该加上一句contain的判断然后再添加。
二、梳理架构设计,并特别分析迭代中对架构的重构
第一次作业只需要按照要求完成即可,无特殊需要注意的。
第二次作业中,MyGraph本应该取继承PathContainer但是,但是自己之前并没有编写接口的习惯,所以在实现的过程中还是将全部的东西重新实现了一次,而没有采用继承。
第三次作业,采用全部重写已经不合适了,因为如果全部都实现的话类会长度非常长。所以这里使用了继承。但是在继承的过程中,为了修改父类的一些函数,所以有些函数需要重写,但是同时又考虑到父类的private变量不能访问,所以又添加了一部分的修改父类变量的函数。
三、梳理JML语言的理论基础
关于这部分介绍可以查看JML官网
1、理论基础
Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为 。它结合了Eiffel的契约方法设计和 Larch 系列接口规范语言的基于模型的规范方法 ,以及细化演算的一些元素 。
契约设计和JML(由Gary T. Leavens和Yoonsik Cheon撰写) 的草案 解释了JML作为Java合同设计(DBC)语言的最基本用法。
下面是一些基本的语法梳理,具体可以选择查看这个大牛的网站
①注释结构块
1 /*@ annotation 2 @ annotation 3 @ annotation 4 @*/
②原子表达式
\result表达式:表示一个非void 类型的方法执行所获得的结果,即方法执行后的返回值。
\old( expr )表达式:用来表示一个表达式expr 在相应方法执行前的取值。【任何情况下,都应该使用\old把关心的表达式取值整体括起来。\old(v.size()) 和\old( v ). size() 有相同的结果但是会倾向使用前者】
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值
\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
\nonnullelements( container )表达式:表示container 对象中存储的对象不会有null
\type(type)表达式:返回类型type对应的类型(Class),如type( boolean )为Boolean.TYPE。
\typeof(expr)表达式:该表达式返回expr对应的准确类型。如\typeof( false )为Boolean.TYPE
③量化表达式
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式:返回给定范围内的表达式的和。
\product表达式:返回给定范围内的表达式的连乘结果。
\max表达式:返回给定范围内的表达式的最大值。
\min表达式:返回给定范围内的表达式的最小值。
④集合表达式
⑤操作符
子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真,如Integer.TYPE<:Integer.TYPE 为真;但Integer.TYPE<:ArrayList.TYPE 为假。需要指出的是,任意一个类X,都必然满足X.TYPE<:Object.TYPE 。
等价关系操作符: b_expr1<==>b_expr2 或者b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是b_expr1==b_expr2 或者b_expr1!=b_expr2 。可以看出,这两个操作符和Java中的==和!= 具有相同的效果,按照JML语言定义, <==> 比== 的优先级要低,同样<=!=> 比!= 的优先级低。
推理操作符: b_expr1==>b_expr2 或者b_expr2<==b_expr1 。对于表达式b_expr1==>b_expr2 而言,当b_expr1==false ,或者b_expr1==true 且b_expr2==true 时,整个表达式的值为true 。【这一种和代码的if是相对应的】
变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。
⑥方法规格
前置条件通过requires子句来表示: requires P;
后置条件通过ensures子句来表示: ensures P;
副作用约束子句,使用关键词assignable 或者modifiable
⑦类型规格
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义invariant P ,
状态变化约束(invariant)只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
2、工具链情况
【官方】断言检查编译器(jmlc),单元测试工具(jmlunit),以及了解JML规范的javadoc(jmldoc)增强版本。
【课程推荐】OpenJML(静态检查)、自动check JML规格文档并生成报告、JMLunitNG(自动化测试)
四、通过JMLUnitNG对graph类进行测试
考虑到无法使用forall和exist。所以我是用了Mypath的修改版
1 import java.util.ArrayList; 2 import java.util.HashSet; 3 import java.util.Iterator; 4 import java.util.Set; 5 6 public class MyPath { 7 // TODO : IMPLEMENT 8 private /*@ spec_public @*/ ArrayList<Integer> nodes; 9 private /*@ spec_public @*/ Set<Integer> distinctnodes; 10 private int hashresult; 11 12 public MyPath(int[] inputnodes) { 13 //System.out.println("mypath"); 14 this.nodes = new ArrayList<Integer>(); 15 this.distinctnodes = new HashSet<>(); 16 hashresult = 0; 17 for (int i = 0; i < inputnodes.length; i++) { 18 this.nodes.add(inputnodes[i]); 19 this.distinctnodes.add(inputnodes[i]); 20 hashresult = (inputnodes[i] % 1024 + hashresult * 307) % 2048; 21 } 22 } 23 24 //@ ensures \result == nodes.size(); 25 public int size() { 26 return this.nodes.size(); 27 } 28 29 /*@ requires index >= 0 && index < size(); 30 @ assignable \nothing; 31 @ ensures \result == nodes.get(index).intValue(); 32 @*/ 33 public /*@pure@*/ int getNode(int var1) { 34 return this.nodes.get(var1); 35 } 36 37 //@ ensures \result == set.contains(i); 38 public boolean containsNode(int var1) { 39 //System.out.println("mypath"); 40 boolean result = false; 41 int nodelistsize = this.size(); 42 for (int i = 0; i < nodelistsize; i++) { 43 if (this.nodes.get(i) == var1) { 44 result = true; 45 break; 46 } 47 } 48 return result; 49 } 50 51 //@ ensures \result == (nodes.size() >= 2); 52 public boolean isValid() { 53 //System.out.println("mypath"); 54 return (this.size() >= 2); 55 } 56 57 public static void main(String[] args) { 58 ; 59 } 60 61 }
执行后结果如下
五、总结心得
JMLNG配置环境极为艰辛。都按着要求做,自己电脑就总是要出幺蛾子。
这次JML这个单元的编程作业主要让我学到了对于一些可能经常访问的内容,要提前做好存储。
而JML这一块,这的的确确是一种很不错的编程习惯,JML进行设计辅助测试、程序再进行实现。但这毕竟是一种非官方的设计模式,检查全靠第三方插件(还是不完善的插件),在版本兼容上多多少少存在问题。在这个问题解决之前,在课程之外的java程序编写时不会愿意去使用junit进行辅助的。相比之下,我更加愿意使用javadoc。