OO第三单元总结
JML理论基础
JML是用于对Java程序进行规格化设计的一种表示语言,是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。JML以javadoc注释的方式来表示规格,每行都以@起头。而JML内容由JML表达式构成。
-
原子表达式
- \result表达式:表示一个非
void
类型方法的执行结果,即表达式的返回值。 - \old(expr)表达式:表示一个表达式expr在执行方法前的取值。
- \not_assigned(x,y,...)表达式:表示括号中的变量在方法执行过程中是否被赋值。
- \not_modified(x,y,...)表达式:与\not_assigned(x,y,...)类似,表示括号中的变量在方法执行期间取值是否发生变化。
- \nonnullelements(container)表达式:表示container对象中存储的元素不会有null。
- \type(type)表达式:返回type对应的类。
- \typeof(expr)表达式:与\type(type)类似,返回expr对应的准确类型。
- \result表达式:表示一个非
-
量化表达式
- \forall表达式:全称量词,相当于一阶逻辑表达式中的\(\forall\),每个元素都需要满足相应的约束。
- \exist表达式:特称量词,相当于一阶逻辑表达式中的\(\exist\),至少存在某个元素满足相应的约束。
- \sum表达式:返回给定范围的表达式的和。
- \product表达式:与\sum表达式类似,返回给定范围的表达式的积。
- \max表达式:返回给定范围的表达式的最大值。
- \min表达式:与\max表达式类似,返回给定范围的表达式的最小值。
- \num_of表达式:返回指定变量中满足相应条件的取值个数。
-
集合表达式
- 集合构造表达式:可以在JML中构造一个局部集合,类似于离散数学中集合的数学表达。
-
操作符
除了可以在JML中使用数学运算符和逻辑关系运算符,还有以下几种运算符。
- 子类型关系操作符:
E1<:E2
,表示类型E1是E2的子类型。 - 等价关系操作符:
b_expr1<==>b_expr2
,表示两个表达式等价。 - 推理操作符:
b_expr1==>b_expr2
,表示第一个表达式可以推出第二个表达式。 - 变量引用操作符:\nothing表示空集,\everything表示全集。
- 子类型关系操作符:
-
方法规格
用来描述一个方法的功能和限制的语句块。
- 前置条件:通常使用requires语句描述,表示方法执行的一些要求约束。
- 后置条件:通常使用ensures语句描述,表示方法执行结果的一些约束。
- 作用范围:通常使用assignable语句描述,表示方法能够修改的变量。
而对于前置条件和后置条件,通常有\normal_behavior和\exception_behavior两种类型,分别对应正常执行和异常。对于异常行为的结果,通常使用\signal语句描述。
-
类型规格
- 不变式invariant:要求可见状态下都满足的约束,描述数据状态应该满足的要求。
- 约束constraint:描述状态变化的满足的约束。
通过JML对程序的方法变量进行形式化描述,减小了设计时错误发生的概率,便于进行测试验证,提高了代码的可维护性。
对于工具链的使用,在这一单元我没有使用与JML有关的工具如OpenJML,主要是许多JML工具能力并不强大,不如自己查看JML。
SMT Solver
SMT Solver是一种验证理论,对应到具体的工具就是OpenJML,这个应用可以验证JML的规格。在部署这个工具的过程中尝试了各种方法,最后依然没有成功运行,遂放弃。随后与同学的交流中得知OpenJML只能检查较为简单的表达式,遇到出现\forall等较为复杂的表达式就会出现报错,就更加坚定了放弃这个工具的想法。目前来看,OpenJML并没有达到能够比人工更准确效率更高的结果,反而花费了大量的时间在配置环境上。
JMLUnitNG
JMLUnitNG是一个通过JML自动生成测试样例并对程序进行自动测试的工具,可以对程序是否符合JML规格进行检查。在部署这个工具的过程中尝试了各种方法依然无果。遂放弃。与同学的讨论交流中得知,这一工具通常生成的数据都是边界数据,如0,int的极限等,这坚定了我不使用这一工具的想法。在这一单元中,我都是通过手动构造特殊数据利用JUnit进行测试,和自动生成随机数据并与同学进行对拍完成的测试,总的来说效果是不错的,JUnit作为一个相对成熟而且使用广泛的测试工具,使用更加简单方便,这里我更推荐使用JUnit对程序进行测试,而非JMLUnitNG。
程序结构分析
-
第一次作业
Method ev(G) iv(G) v(G) MainClass.main(String[]) 1 1 1 MyNetwork.MyNetwork() 1 1 1 MyNetwork.addPerson(Person) 3 2 3 MyNetwork.addRelation(int,int,int) 4 7 9 MyNetwork.compareAge(int,int) 2 2 3 MyNetwork.compareName(int,int) 2 2 3 MyNetwork.contains(int) 3 2 3 MyNetwork.dfs(int,int,ArrayList ,ArrayList ) 5 5 7 MyNetwork.getPerson(int) 3 2 3 MyNetwork.isCircle(int,int) 2 2 3 MyNetwork.queryAcquaintanceSum(int) 2 1 2 MyNetwork.queryConflict(int,int) 2 2 3 MyNetwork.queryNameRank(int) 2 2 4 MyNetwork.queryPeopleSum() 1 1 1 MyNetwork.queryValue(int,int) 3 4 6 MyPerson.MyPerson(int,String,BigInteger,int) 1 1 1 MyPerson.addAcquaintance(Person,int) 1 1 1 MyPerson.compareTo(Person) 1 1 1 MyPerson.equals(Object) 3 2 4 MyPerson.getAcquaintanceSum() 1 1 1 MyPerson.getAge() 1 1 1 MyPerson.getCharacter() 1 1 1 MyPerson.getId() 1 1 1 MyPerson.getName() 1 1 1 MyPerson.hashCode() 1 1 1 MyPerson.isLinked(Person) 4 2 4 MyPerson.queryValue(Person) 3 3 3 MainClass 1 1 MyNetwork 2.64 37 MyPerson 1.58 19 这里我只列出了作业中实现的类。第一次作业中只需要实现
Person
和NetWork
两个接口,即这里的MyPerson
和MyNetwork
。第一次作业要求并不高,所以基本是按照JML来写,保证了功能正确。所有方法中复杂度较高的是dfs,用来检查两个人是否连通,这里就是朴素的深度优先搜索算法。从扩展性来说,这份代码耦合较少,对于将来有可能的添加关系、删除关系、删除人等可能的操作在不对性能有较高要求的情况下可以很方便的添加。 -
第二次作业
Method ev(G) iv(G) v(G) MainClass.main(String[]) 1 1 1 MyGroup.MyGroup(int) 1 1 1 MyGroup.addPerson(Person) 1 3 3 MyGroup.addRelation(int) 1 1 1 MyGroup.equals(Object) 3 2 4 MyGroup.getAgeMean() 2 1 2 MyGroup.getAgeVar() 2 1 2 MyGroup.getConflictSum() 1 1 1 MyGroup.getId() 1 1 1 MyGroup.getPeopleSum() 1 1 1 MyGroup.getRelationSum() 1 1 1 MyGroup.getValueSum() 1 1 1 MyGroup.hasPerson(Person) 1 1 1 MyGroup.hashCode() 1 1 1 MyNetwork.MyNetwork() 1 1 1 MyNetwork.addGroup(Group) 2 1 2 MyNetwork.addPerson(Person) 2 1 2 MyNetwork.addRelation(int,int,int) 4 10 12 MyNetwork.addtoGroup(int,int) 5 1 5 MyNetwork.compareAge(int,int) 2 2 3 MyNetwork.compareName(int,int) 2 2 3 MyNetwork.contains(int) 1 1 1 MyNetwork.getGroup(int) 1 1 1 MyNetwork.getPerson(int) 1 1 1 MyNetwork.isCircle(int,int) 5 5 8 MyNetwork.queryAcquaintanceSum(int) 2 1 2 MyNetwork.queryConflict(int,int) 2 2 3 MyNetwork.queryGroupAgeMean(int) 2 1 2 MyNetwork.queryGroupAgeVar(int) 2 1 2 MyNetwork.queryGroupConflictSum(int) 2 1 2 MyNetwork.queryGroupPeopleSum(int) 2 1 2 MyNetwork.queryGroupRelationSum(int) 2 1 2 MyNetwork.queryGroupSum() 1 1 1 MyNetwork.queryGroupValueSum(int) 2 1 2 MyNetwork.queryNameRank(int) 2 2 4 MyNetwork.queryPeopleSum() 1 1 1 MyNetwork.queryValue(int,int) 3 4 6 MyPerson.MyPerson(int,String,BigInteger,int) 1 1 1 MyPerson.addAcquaintance(Person,int) 1 1 1 MyPerson.compareTo(Person) 1 1 1 MyPerson.equals(Object) 3 2 4 MyPerson.getAcquaintance() 1 1 1 MyPerson.getAcquaintanceSum() 1 1 1 MyPerson.getAge() 1 1 1 MyPerson.getCharacter() 1 1 1 MyPerson.getId() 1 1 1 MyPerson.getName() 1 1 1 MyPerson.hashCode() 1 1 1 MyPerson.inGroup(int) 1 1 1 MyPerson.isLinked(Person) 2 1 2 MyPerson.joinGroup(Group) 1 1 1 MyPerson.queryValue(Person) 2 1 2 MainClass 1 1 MyGroup 1.46 19 MyNetwork 2.39 55 MyPerson 1.27 19 第二次作业需要实现新增的
Group
接口,即这里的MyGroup
,同时NetWork
中也添加了许多新的需要实现的方法。这次作业中,大部分的方法我也依然对照JML书写,保证了正确性。其中有两个复杂度较高的方法,isCircle
和addRelation
,考虑到未来可能的删除关系的新增需求,isCircle
我采用的是bfs算法,而不是类似于并查集那样维护一个连通块。对于addRelation
,考虑到性能的问题,需要在新增关系,新增人的时候更新Group
中的相关属性,通过均摊的方法降低复杂度,导致这一方法复杂度较高且耦合较高。这也带来了程序出bug的隐患。 -
第三次作业
Method ev(G) iv(G) v(G) MainClass.main(String[]) 1 1 1 MyBlock.MyBlock(int,Person) 1 1 1 MyBlock.Pair.Pair(Person,int) 1 1 1 MyBlock.Pair.compareTo(Pair) 3 1 3 MyBlock.findCircle(Person,Person) 3 6 7 MyBlock.findMin(Person,Person) 5 5 7 MyBlock.getPeople() 1 1 1 MyBlock.getSize() 1 1 1 MyBlock.mix(MyBlock) 1 2 2 MyBlock.tarjan(Person,Person,HashMap ,HashMap ,Stack ,Stack ,HashMap ,HashMap ) 6 14 15 MyGroup.MyGroup(int) 1 1 1 MyGroup.addPerson(Person) 1 3 3 MyGroup.addRelation(int) 1 1 1 MyGroup.delPerson(Person) 1 3 3 MyGroup.equals(Object) 3 2 4 MyGroup.getAgeMean() 2 1 2 MyGroup.getAgeVar() 2 1 2 MyGroup.getConflictSum() 1 1 1 MyGroup.getId() 1 1 1 MyGroup.getPeopleSum() 1 1 1 MyGroup.getRelationSum() 1 1 1 MyGroup.getValueSum() 1 1 1 MyGroup.hasPerson(Person) 1 1 1 MyGroup.hashCode() 1 1 1 MyNetwork.MyNetwork() 1 1 1 MyNetwork.addGroup(Group) 2 1 2 MyNetwork.addPerson(Person) 2 1 2 MyNetwork.addRelation(int,int,int) 4 12 14 MyNetwork.addtoGroup(int,int) 5 1 5 MyNetwork.borrowFrom(int,int,int) 3 3 5 MyNetwork.compareAge(int,int) 2 2 3 MyNetwork.compareName(int,int) 2 2 3 MyNetwork.contains(int) 1 1 1 MyNetwork.delFromGroup(int,int) 4 1 4 MyNetwork.getGroup(int) 1 1 1 MyNetwork.getPerson(int) 1 1 1 MyNetwork.isCircle(int,int) 2 2 3 MyNetwork.queryAcquaintanceSum(int) 2 1 2 MyNetwork.queryAgeSum(int,int) 1 3 4 MyNetwork.queryBlockSum() 1 1 1 MyNetwork.queryConflict(int,int) 2 2 3 MyNetwork.queryGroupAgeMean(int) 2 1 2 MyNetwork.queryGroupAgeVar(int) 2 1 2 MyNetwork.queryGroupConflictSum(int) 2 1 2 MyNetwork.queryGroupPeopleSum(int) 2 1 2 MyNetwork.queryGroupRelationSum(int) 2 1 2 MyNetwork.queryGroupSum() 1 1 1 MyNetwork.queryGroupValueSum(int) 2 1 2 MyNetwork.queryMinPath(int,int) 4 6 9 MyNetwork.queryMoney(int) 2 1 2 MyNetwork.queryNameRank(int) 2 2 4 MyNetwork.queryPeopleSum() 1 1 1 MyNetwork.queryStrongLinked(int,int) 4 3 6 MyNetwork.queryValue(int,int) 3 4 6 MyPerson.MyPerson(int,String,BigInteger,int) 1 1 1 MyPerson.addAcquaintance(Person,int) 1 1 1 MyPerson.compareTo(Person) 1 1 1 MyPerson.delGroup(int) 1 1 1 MyPerson.equals(Object) 3 2 4 MyPerson.getAcquaintance() 1 1 1 MyPerson.getAcquaintanceSum() 1 1 1 MyPerson.getAge() 1 1 1 MyPerson.getCharacter() 1 1 1 MyPerson.getId() 1 1 1 MyPerson.getName() 1 1 1 MyPerson.getRoot() 1 1 1 MyPerson.hashCode() 1 1 1 MyPerson.inGroup(int) 1 1 1 MyPerson.isLinked(Person) 2 1 2 MyPerson.joinGroup(Group) 1 1 1 MyPerson.queryValue(Person) 2 1 2 MyPerson.switchRoot(int) 1 1 1 MainClass 1 1 MyBlock 4 28 MyBlock.Pair 2 4 MyGroup 1.57 22 MyNetwork 2.43 73 MyPerson 1.22 22 第三次作业没有新增类,但新增了查找最短路径、查询连通块数量、查询环路、删除组和其它一些新的方法,难度上升了不少,对性能的要求也更高了。可以看出,这次作业复杂度较高的几个功能就是连通性检测、最短路径、连通块数量、检测环路这四个功能,由于没有删除关系这一功能,我采用了并查集的思路,新设计一个连通块类
MyBlock
,将处在一个连通块内的人进行管理,使得连通块数量的查询和连通性查询复杂度极大降低,同时也缩小了最短路径、环路查询的范围。对于最短路径查询,由于新增人新增关系的原因,导致用弗洛伊德算法维护一个最短路径矩阵的方法并不适用,因此我采用了堆优化的dijkstra算法,复杂度在O(nlogn)。对于环路的查询有三种思路,一种是采用tarjan算法,求解图的点双连通分量,复杂度是O(n);一种是两次dfs求解环路,但这一方案很容易出bug,于是放弃;最后一种就是通过dfs暴力枚举所有路径,再检查,这种方法不太容易出bug,但复杂度较高,也放弃;最终采用了tarjan算法。从架构上来说这样的设计更多的照顾到了性能,但几个类的耦合度很高,比如如果需要新增删除关系的功能,这一套架构是很难维护的,因此从扩展性来说,这并不是一个特别好的设计。
bug分析
-
第一次作业
第一次作业功能上并没有问题,但由于dfs实现方式不对,没有记录已经访问过的节点,只记录了当前路径,导致复杂度极高,强测tle。说明课下还是需要做大数据的测试才行。
-
第二次作业
第二次作业功能上并没有问题,由于考虑到减小架构的耦合,因此没有维护组内的关系总数等属性,导致复杂度达到了O(n^2),因此强测tle了一个点,互测也被捅了一刀。互测中其他人的bug也基本都出在这里。
-
第三次作业
第三次作业强测互测并没有发现bug,屋内也没有被刀出bug。采用了并查集和tarjan算法性能上是过关的。互测过程中我发现了自己的程序的一个bug,也就是在将人从组里面删除的时候没有更新人所在组的信息,这就是耦合带来的坏处,是的一些信息难以维护,容易出bug,只是这次侥幸没有被找出来。
心得体会
这一单元我学习了基于JML的规格化设计,理解了如何利用JML形式化地描述数据的一些性质和约束,认识了规格设计的基本方法和重要意义。也学会了利用JUnit和JML对方法进行单元测试,检查程序的正确性,这一点对我第三次作业的完成帮助巨大。
OO接近尾声,最后一个单元继续加油!