相对于前两单元,这一单元是窝做的最不理想的一单元了,其中的原因有很多。值得一提的是除了本人确实是菜没有想出好的算法之外,还有一点就是着实手残了。由于这一单元需要大量的数据的取出、计算与存储等操作,所以作为手残党的我简直要枯了。哎~满屏皆凉凉。下面是我对这一单元三次作业的总结(希望在写博客的时候不要手残)。
一 JML语言总结
JML是一种详细设计的符号语言,鼓励我们用一种全新的方式来看待java的类和方法。我们OO课的一个重要原则就是过程性的思考应该尽可能的推迟。在编写java程序的时候应用JML语言改进我们的程序。下面我们来看一下JML的语法。
1.基础语法
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式 为 //@annotation ,块注释的方式为 /* @ annotation @*/ 。其中在每个注释块中的JML语言大致可以分为以下三个部分:
(1)requires子句定义该方法的前置条件(precondition),指在该方法执行之前变量需要满足的条件。
(2))副作用范围限定,assignable列出这个方法能够修改的类成员属性,\nothing是个关键词,表示这个方法不对 任何成员属性进行修改,所以是一个pure方法。
(3)ensures子句定义了后置条件,即执行完该方法之后变量或者类成员应该满足的条件。
2.JML表达式
(1)原子表达式
\result : 表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
\old(expr) : 用来表示一个表达式 expr 在相应方法执行前的取值.
\not_assigned(x,y,…) : :用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false 。
\not_modified(x,y,…) : 与not_assigned类似,该表达式限制括号中的变量在方法执行期间的取 值未发生变化。
\nonnullelements( container ) : 表示 container 对象中存储的对象不会有 null.
\type(type) : 返回类型type对应的类型(Class),如type( boolean )为Boolean.
\typeof(expr) : 该表达式返回expr对应的准确类型。如\typeof( false )为Boolean.
(2)量化表达式
\forall : 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists : 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\max : 返回给定范围内的表达式的最大值。
\min : 返回给定范围内的表达式的最小值。
(3)集合表达式
集合构造表达式 : 集合构造表达式的一般形式为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合 中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
(4)操作符
子类型关系操作符 : E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真.
等价关系操作符 : b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达 式,这两个表达式的意思是 b_expr1==b_expr2 或者 b_expr1!=b_expr2 。
变量引用操作符 : \nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变 量。变量引用操作符经常在assignable句子中使用,如 assignable \nothing 表示当前作用域下每个变量都不可以 在方法执行过程中被赋值。
3.方法规格
前置条件:通过requires子句来表示: requires P; 。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
后置条件:通过ensures子句来表示: ensures P; 。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执 行返回结果一定满足谓词P的要求,即确保P为真”。
副作用范围限定:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法 规格的角度,必须要明确给出副作用范围。关键词:assignable(可赋值);modifiable(可修改)。
signals子句:signals (***Exception e) b_expr ,意思是当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e。
4.类型规格
(1)不变式限制(invariant)
不变式是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中 invariant 为关 键词, P 为谓词。其中,下面定义的几种状态都为可见状态。
对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
在未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
在未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻
(2)状态变化约束(constraint)
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定 invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的 关系进行约束。
和不变式一样,JML也根据类的静态成员变量区分了两类约束:static constraint和instance constraint。其中static constraint指涉及类的静态成员变量,而instance constraint则可以涉及类的静态成员变量和非静态成员变量。同 样,也可以在规格中通过关键词来明确加以区分: static constraint P 和 instance constraint P 。
二 JMLUnit测试
测试代码如下:

1.首先用openjml对该规格进行正确性检测,如下图所示,可以看到一开始由于没有加分号而报错,改完之后便没有问题了。

2.接下来使用JMLUnitNG测试
本地编译生成的文件如下:

进一步编译如下:

可以看到因为构造的方法比较简单,所以并没有检测出Failure。但是在我们所编写的比这个规模要大一些的程序来讲,那么我们便有可能会出现各种bug。由此看来这样的测试的确可是一中很好的选择。
三 作业分析
1.第一次作业
思路
第一次作业要求比较简单,主要是实现一个MyPath类来表示路径与一个MyPathContainer类来存储已经add进来的路径。
在MyPath类中,考虑到一个Path在构建完成之后便不会对其进行节点的增删等操作,因此只是用了一个ArrayList链表来存储了该Path的结点。而对于需要实现的getDistinctNodeCount方法则构建一个HashMap存储已经加进去的结点。对于该Path的一个新的结点,如果HashMap中未包含该结点,则加进去。(此处其实可以直接用一个HashSet来实现的。)
在MyPathContainer类中,构建有三个HashMap。其中两个是为了存储add进入的Path,格式如下:<id, Path> 与 <Path.hashcode(), id>(此处我在path类中重写了Hashcode方法,返回的是Path中的ArrayList的Hashcode)。另外一个是为了处理 getDistinctNodeCount方法的,处理过程与Path中类似,在每次add和remove的时候改变,格式为<nodeid, nodecount>。
bug
这里就是象征着这一单元红红火火开始的第一个手残bud了。前面有说到,在PathContainer中我以<nodeid, nodecount>的HashMap的形式来处理得到不同结点个数的方法。在每次增加或者删除路径的时候改变结点的nodecount。然而我在remove的时候在取出nodecount进行减1之后没有再加进容器中,嗯~~~
第一次作业架构

2.第二次作业
思路与重构分析
第二次作业延用了第一次的MyPath与MyPathContainer类,MyGraph类在继承了MyPahContainer的基础上增加了isConnected、getShortestPathLength等方法。其实理解起来就是在第一次作业实现了一个存储有Path容器的基础上构建一个Map,该Map包含各个节点之间的关系。
我在MyGraph中主要建立了两个核心的HashMap,一个是map,负责在每次addpath和removepath的时候建立起各个节点之间的直接连接的关系,其格式为<fromNode, <toNode, count>>,此处的count含义是有count条边。这样在每次add时count+1,在每次remove时count—, 而当count = 0时便删除两个节点之间的边。另外一个HashMap是disNode,负责存储两个节点之间的最短距离,其格式为<fromNode, <toNode, distance>>,在每次计算两个结点之间的最短距离时,先查询该HashMap,若没有相应的存储数据则进行计算,同时将计算出的结果存入该HashMap中。
在查询两个节点之间的最短距离时,我使用的是bfs大法。构建两个HashMap,一个(举例为 A)负责存储正在遍历的结点,另一个(举例为 B)存储在当前节点所能到达的结点,在A遍历完之后,再对B进行相当于下一层的遍历。
在这一次作业中指导书提示过建议专门构建一个Map的类来实现Path实现的图,但是我没有采取。。因为当时想的是这次作业要求实现的功能简单,没必要另外构建一个Map类。再加上当时猜测第三次作业也就是有权图加上有向图的应用了,所以就没有专门去开一个类。这就埋下了需要重构的隐患。哎~奈何我还是太naive
bug
怎么说呢,这次的bug其实主要是自己的知识储备不够在第一次作业中就已经有所体现了。因为作业中要求实现的方法有一些需要抛出异常,而有一些又不需要抛出异常,自己一开始在怎么抛出异常这一方面理解不到位,大概是根据idea的报错编程。后来理解的是如果在定义方法的时候throw 了一个异常,那么在方法中就应该throw new一个异常。
然而在写第二次作业的时候我开始意识到其实一个方法调用了能够与其抛出同样异常的方法时,该方法定义的throw 的异常就能够实现,完全不需要再像之前那样啰啰嗦嗦的写了,因此我就把第一次作业写的多余的地方都给删除了。而在对remove方法进行删除改写的时候,我把原来写的Path.hashcode给改成了Path,这就导致在删除的时候没有办法从容器中删除对应的那个Path,这就是bug所在了。
第二次作业架构

3.第三次作业
思路与重构分析
第三次作业实现了MyPath与MyRailwaySystem两个类。其中MyPath没有变化,MyRailwaySystem继承了第二次作业的MyGraph类。查看MyRailwaySystem的规格可以看到这次主要是要求实现最短换乘、最少不满意度与最少票价等问题。
很明显这次的作业不是简简单单的一个图就能实现的了,所以我选择新构建了一个MyMap类,其中关键的有两个hashMap,一个为map,格式为<fromNode, <toNode, weight>>,其中weight代表点与点之间的距离(即权重)。另一个为reply,顾名思义,该HashMap用来存储已经计算出来的点与点的最短(少)票价(满意度)。
下面就来介绍一下我这次作业选择的构造方法了。在经过几天的烧脑之后我还是选择了讨论区里面的以为巨巨分享的方法。大致意思为将每个Path构造成为一个图,该图的为完全无向图,点与点之间的距离代表在该Path中的最短路径。这样最终将这些Path构造成的图合并为一个图,那么就可以说从一个结点到另一个结点,每经过一条边就代表一次换乘,以此来实现在计算票价与不满意度的换乘问题。至于途径路线的不满意度或者是票价,则用构造成的图的权重来表示。举个例子:如对于Path 1 2 3 与 Path 3 4 5来讲,分别对Path构建图,如下:

另外由于这次我是采用构建有向图的方式做的,因此我在这次的作业中还专门构建了一个狄杰斯特拉的计算类,用于计算两个节点的最短距离。在该类中维护了一个优先队列,在一定程度上简化了计算。
其实说实话这次作业由于第二次作业的架构没有设计好写的蛮难受的,因为第二次作业并没有一个专门的Map类,而这次理论上讲的话应该也要把第二次用来算最短路径的图也用Map类来构造,这样的话代码的可拓展性强一些。否则如果有第四次作业的话恐怕会因为结构过于混乱而出事情。
bug
这里还是先说一下手残bug吧。道理上说,每算出两个节点之间的最短距离重新存储进入容器时,应该正着存一遍然后反着再存一遍。而在这反着存的时候本应该是distance的我给存储成了1。
还有一个比较严肃的bug就是算法上的问题了。通过刚刚的分析可以看到我采用的算法在对Path构图的时候需要对Path的每个节点都要遍历一遍全部节点,这一方面会耗费大量的时间。而且这种方法仍然需要在每次add或者remove的时候重新构图,因此我在初步编写的时候采用的是在每次add或者remove的时候先不重构图,在每次查询需要用的时候再考虑是否需要重构图。
然而这样还是被互测的🐺人给hack了,大致就是我每次add一次,然后查询一次,这样不断add不断查询就是TLE了。初步的解决方案是在每次add的时候先判断我们的图是否需要重构,不需要的话只用在已有的图的基础上增加该Path生成的图即可,这样就可以避免了一些多余的大量计算。
第三次作业架构
