OO Unit3 总结
目录
1 JML概述
1.1 JML思想
1.2 JML语法
2 JMLUnit测试
3 架构设计
3.1 第一次作业
3.2 第二次作业
3.3 第三次作业
4 bug分析
4.1 第一次作业
4.2 第二次作业
4.3 第三次作业
5 心得体会
1 JML概述
1.1 JML思想
JML是一种描述规格的语言,它用形式逻辑的公式描述Java程序中数据和方法的性质,从而对Java类的实现进行规定。JML有以下作用:
1. JML将上层功能的设计与下层实现分离。这使得类的实现被推迟考虑,设计者能够首先独立地设计上层功能,尔后交给其他人来实现。
2. JML用形式逻辑的方式表示。如果规格本身的设计有错误,可以通过对逻辑公式的演算发现这样的错误,并且这样的过程是可计算的。
3. 基于JML可以进行与实现无关的单元测试。这使得类的测试与实现也分离了,有利于进行测试驱动开发。
4. 类的使用者可以通过阅读JML了解类的功能。这时使用者不需要阅读全部的代码,也不需要了解类的实现。
总而言之,规格的思想是将开发工作划分为设计与实现两个层次,降低两个层次之间的耦合度,从而对上层设计者和用户屏蔽下层实现。可以将规格看作设计者与用户之间的一个协议,而这个协议是不经过实现者的。
1.2 JML语法
对于每一个有规格的方法,它的规格可以分为多个情况,这些情况的并应当是完备的。情况分为两种:正常情况、异常情况。
public_normal_behavior表示正常情况。正常情况可以分为三个部分:前置条件、后置条件、副作用。
require后的逻辑公式表示方法的前置条件,描述该情况下调用方法时需要满足的条件。
ensure后的逻辑公式表示方法的后置条件,描述调用方法后产生的影响。后置条件中用\result表示方法的返回值,用\old后的变量表示该变量在调用方法之前的值。
副作用与后置条件的区别在于:后置条件是由设计者规定的,是确定的条件,因此在规格语法中用逻辑公式表示;副作用对于设计者而言可能是不确定的,与实现者的实现方式有关。设计者只规定哪些数据能够被修改,因此用assignable后的变量表示这些变量可能被修改。副作用中用\nothing表示不能修改任何数据,意义与用pure修饰方法相同。
public_exceptional_behavior表示异常情况。异常情况有前置条件,若没有写出,则缺省为正常情况的补。异常情况下,方法只会抛出异常,而不执行任何功能,因此也就没有后置条件和副作用。
描述规格时,需要用形式逻辑的公式表示。因此形式逻辑中的符号都在JML语法中有其对应:
!、&&、||分别表示否定、合取、析取,与它们在Java语言中的意义相同
=>、<==>分别表示蕴含、等价
\forall、\exists分别表示任意、存在
2 JMLUnit测试
3 架构设计
3.1 第一次作业
第一次作业中的查询大多都是对单个节点的,都比较简单,只需要读懂规格就可以很容易地编写。只有isCircle方法需要用到整个图的信息,我用并查集来实现它,在添加边时更新并查集。
因此在第一次作业中,除了另外编写了UnionFind类维护并查集之外,其他的方法几乎是按照规格编写的。
3.2 第二次作业
第二次作业中加入了分组的概念,对组的查询都是以“求和”的形式进行的。因此只需要单独维护这些和,在向组中添加节点和向图中添加边的时候更新它们,就可以避免每次查询时都进行计算,从而提升性能。此外,我意识到第一次作业中的queryNameRank方法的实现采用遍历的方式,性能不高,因此改用平衡二叉树实现。(反而因此引入了bug)
在第二次作业中,保留了UnionFind类维护并查集,记录图中的连通性;此外编写了AvlTree类维护平衡二叉树,记录节点的name排序。其他方法几乎是按照规格编写的。
3.3 第三次作业
第三次作业中加入了查询最短路径、查询点双连通性的操作,这两个操作都要用到整个图的信息。最短路径方面,因为没有考虑Dijkstra算法的堆优化,所以我最后放弃了Dijkstra算法,而采用查询时dfs,大大损失了性能。此外,考虑到本次作业中图的边只增不减,所以我自己设计了一个算法,来记录图中的点双连通性,同时也记录了连通性,替代了并查集。但由于我设计这个算法时,想法并不成熟,因此实现时产生了许多bug。在经过多次测试后,代码中仍然存在着许多致命的bug,因此没有取得好的成绩。
我的算法大致思想是这样的(证明略去):
1. 若一个图中没有割点,则称为H图。显然,两个节点间有一条边的图是H图;3阶以上的H图是点双连通图。因此,图中两个点是点双连通的充分必要条件是它们同属于一个不小于3阶的H子图。
2. 任意一个图都可以唯一地表示成若干个极大H子图的并,成为极大H子图分割。
3. 两个极大H子图之间要么没有公共点,要么恰有一个公共点,且这个公共点是割点。因此可以用图的极大H子图分割作为点集,两个极大H子图之间的公共点作为边,建立另一个图来描述点双连通性,这样的图称为H分割图
4. 一个图的H分割图中可能包含一些完全子图,但如果将这些完全子图压缩为一点,可证明这样得到的图是一个树林。
利用这样的性质,就可以维护一个树林,其节点是所有的极大H子图。当添加节点或边时,需要经过一些变换,在这个树林中添加节点或边;若引入了环,则需要将环压缩为一点。这样的就可以高效地维护点双连通分支,同时也顺便维护了连通分支。
在第三次作业中,取消了UnionFind类,代之以HGraph类维护H分割图,记录图中点双连通性的同时也记录了连通性;取消了AvlTree类,代之以SortedList类维护有序列表,支持二分查找。其他方法几乎是按照规格编写的。
4 bug分析
4.1 第一次作业
第一次作业我除了并查集外没有使用其他复杂的数据结构,几乎全都是按照规格编写的,强测得到了100分,因此没有修复bug。
4.2 第二次作业
第二次作业为了优化queryNameRank方法,编写了AvlTree,但引入了bug。经过一番修改后,虽然修复了bug,但性能却下降了,没有达到我想象中的标准。因此在后续的作业中取消了AvlTree类,代之以SortedList类,进行简单的二分插入和二分查找。虽然不如平衡二叉树那么稳定,但总比直接遍历的性能要好。
4.3 第三次作业
第三次作业中的bug主要来自我设计的HGraph类。在设计时,我只做了数学上的证明,而没有考虑实现。所以在编写代码时有些不知所措,出现了很多bug。对于我自己“发明”的一些数据结构,也没有思考得很清楚。经过修改后,虽然能够得到正确结果,但性能却十分低下,因此我放弃了这个算法,计划采用Tarjan算法重新实现点双连通性的查询。
5 心得体会
规格对于实现者来说,可以说是一种约束,使得实现者不能完全自由地实现功能;但对于设计者和用户而言,其意义是重大的。在编写规格时,需要明确方法的功能和可能引起的副作用,使得规格具有可靠性和完备性。在编写有规格的类时,需要仔细阅读规格,理解规格描述的功能,使得实现与规格相符合。
此外,我还体会到设计一个算法的不易,不仅需要数学上严谨的证明,还需要考虑到实现的细节,以及实际上能否达到理论中的复杂度。