2020北航面向对象第三单元总结
JML语言基础及工具链应用
什么是JML
官方:JML是一种形式化的、面向JAVA的行为接口规格语言,使用Javadoc的注释方式
个人理解:我认为的JML就像是一份完整的计划书,用户的需求可能有很多,用户很多情况下也只有模糊的需求,对于边界等更不是用户需要考虑的,而在我看来JML就像是根据需求更清晰化的制定了一份计划,而我们写代码的过程就是实践计划的过程。但JML也只是清晰的告诉你什么情况该有什么效果,而不是直接告诉你该怎么做。(错误地理解导致第二次作业没进互测)
JML本身代表的是一种规格和规范,规定你能做什么不能做什么,这种规格的实现,使得具体的实现方式千差万别,实现的效果却殊途同归,甚至在细节和边界上都是一致的。
JML基础语法
语法 | 含义 |
---|---|
requires P | 前置条件需要满足P(前置条件) |
ensures P | 在过程未被requires排除时执行需要满足的效果(后置条件) |
assignable list | 定义哪些成员变量在方法执行中可以被改变(副作用) |
signal (Exception e) P | 当前置条件满足P时,抛出异常e |
Invariant | 要求一个参量在所有可见状态下需要满足的条件 |
Constraint | 要求一个参量在状态变化时需要满足的条件 |
\result | 非void方法的返回值 |
\old(E) | 表达式E在方法实现前的取值 |
/* @spec_public@ */ | 需创建为私有成员变量 |
/* @not_null@ */ | 该成员变量不可为空 |
//@ public model Type | 声明规格变量 |
(\forall T x; R(x);P(x)) | 是否满足条件R(x)的x都使得P(x)成立 |
(\exists T x; R(x);P(x)) | 是否存在满足条件R(x)的x使得P(x)成立 |
(\num_of T x; R(x);P(x)) | 满足R(x)的使得P(x)成立的x的数量 |
(\sum T x; R(x);E) | 满足R(x)的x对应表达式E的和 |
public normal_behavior | 正常情况 |
public exceptional_behavior | 异常情况,通常根据情况不同抛出不同的异常 |
以上就是在JML中主要使用的语法
应用的工具链
本人在工具链的搜索使用方面一直不是很擅长,属于伸手党。。,在本单元中也只是在第二次实验中使用了junit进行了方法的测试,没用使用更多的工具链。
JMLUnitNG测试情况
使用JMLUnitNG自动生成测试用例测试情况如下
[TestNG] Running:
Command line suiteFailed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed:.addPerson(null)
Failed:.addPerson(null)
Failed:.addPerson(null)
Passed:.addRelation(-2147483648)
Passed:.addRelation(-2147483648)
Passed:.addRelation(-2147483648)
Passed:.addRelation(0)
Passed:.addRelation(0)
Passed:.addRelation(0)
Passed:.addRelation(2147483647)
Passed:.addRelation(2147483647)
Passed:.addRelation(2147483647)
Failed:.delPerson(null)
Failed:.delPerson(null)
Failed:.delPerson(null)
Passed:.equals(null)
Passed:.equals(null)
Passed:.equals(null)
Passed:.equals(java.lang.Object@270421f5)
Passed:.equals(java.lang.Object@4f4a7090)
Passed:.equals(java.lang.Object@6956de9)
Passed:.getAgeMean()
Passed:.getAgeMean()
Passed:.getAgeMean()
Passed:.getAgeVar()
Passed:.getAgeVar()
Passed:.getAgeVar()
Passed:.getConflictSum()
Passed:.getConflictSum()
Passed:.getConflictSum()
Passed:.getId()
Passed:.getId()
Passed:.getId()
Passed:.getPeopleSum()
Passed:.getPeopleSum()
Passed:.getPeopleSum()
Passed:.getRelationSum()
Passed:.getRelationSum()
Passed:.getRelationSum()
Passed:.getValueSum()
Passed:.getValueSum()
Passed:.getValueSum()
Failed:.hasPerson(null)
Failed:.hasPerson(null)
Failed:.hasPerson(null) ===============================================
Command line suiteTotal tests run: 49, Failures: 10, Skips: 0
===============================================
Process finished with exit code 0
生成的测试样例主要对于输入空以及边![\Work\jml\week4\Picture.png)界进行了测试,测试结果来看,未通过的方法是addPerson、delPerson和hasPerson,因为输入null而无法处理,但在上述前两个函数的调用处都已经首先保证了输入不会是null,因此实际测试可以算是通过了,hasPerson应当对null进行特判,但本次作业不会出现nullPerson的情况,因此没有出现问题但应当改正。
架构设计
本单元的三次作业全部是迭代完成的,并且每次的作业并不需要对已经实现的部分进行修改也可以完成任务(为了优化而重新实现除外),因此只展示并分析第三次作业。
MyPerson,MyGroup,MyNetWork分别实现对应的接口,MyPerson作为一个本次作业中“最小”的个体可以看到并不依赖于其他的类,被其他的类所依赖和管理,这种模块化的实现方式让我在debug的时候感受到了很多好处,也算是我一次在规格的帮助下真正实现”面向对象“,MyGroup对其MyPerson的Person进行着管理,而MyNetWork则对所有Person和Group进行管理,Main方法新建runner线程并执行,runner完成对输入的获取及处理。除开以上比必须实现的类,我只单独新建了Node类用于保存在最小路径搜索的优先队列内,这个类的作用更类似于一个结构而非一个类,使用范围也仅限于qmp一个方法之内。
这次作业的难点基本集中于iscircle,qmp,qsl,以及qbs,iscircle由于我通过并查集实现,较为安全同样使得qbs非常简单安全,不做详细的叙述,而qmp和qsl都分别通过了一些点但是也损失了一些点,前者通过堆优化的迪杰斯特拉算法后者通过点排除的方法,产生的问题在bug分析部分详细叙述。
底层的容器开始使用ArrayList实现,但是本单元的作业在空间上不做要求而在时间上严格把控,因此之后将容器全部修改为HashMap来进行保存,但是有一点实现较为不好,在存储时我的HashMap的键全部采用对应Person的id,因为id独一无二因此具有可实现性,但是具体操作时产生了问题,也在bug部分详细叙述。
总体而言,由于规格的存在,使得本次作业的条理十分的清晰,自己在书写代码时也能够明显的感觉到脉络清晰,思维连贯,写起来非常的舒服(排除算法实现部分是这样的),本单元的架构相比之前两个单元提升很多,虽然很大程度是因为助教和老师们使用了规格进行了限制和帮助。
本单元bug以及修复情况
第一次作业我认为较为简单,发现的bug也多是笔误或是没想清楚,不做详述。
第二次作业直接不存在此互测成员,强测公布才发现ctle了16个点,将ArrayList改位HashMap实现即可,主要的原因集中于contains以及get这两个方法,HashMap采用哈希值进行搜索,牺牲空间换取了时间,而ArrayList的O(n)则完全是危险区,以上两个方法都要完全遍历整个数组,一一比对导致最后的超时,这也是我对于时间轻视的结果,擅自认为本单元仅仅只是考察对规格的实现,没有对架构以及实现做过多的思考,其实强测结果出来马上就能想到问题说明其实本来有能力改正,contains和get方法在整个network中是使用非常频繁的,甚至于在对acquaintance进行遍历时每次都要使用而达到O(n^2)的复杂度,这次没进入互测也算是自食恶果。
第三次的bug修复过程可以说是非常坚艰辛了。
在第三次的bug修复中我提交了很多个版本,因此最终的版本有很多的修改,但实际当中发挥作用的只有ArrayList以及在qsl中的清零
在修复bug时我首先发现14-16都是考察qmp,因此对qmp进行了修改,首先是将arrive的作用由“记录已经到达的点”改为“记录能够到达的点”,这样在每次接收可到达点的时候我无需将这些点全部加入优先队列,只是将可到达点的距离小于已经在arrive中记录的距离或是没在arrive中出现的点进行加入并且更新arrive,这样就减少了优先队列中元素的数量也就减少了遍历次数,但是很遗憾,效果不好,进行分析这种只是减少了在稠密图中的遍历次数,并且遍历这些多余点时的每次遍历复杂度都是O(1),因此只是能够修复一个点。
第二次改进我主要对记录方式进行改变,在之前的arrive记录以及优先队列记录时,我将Person的id取出来当作索引进行保存,但是这样的结果就是每次我在取索引之后都要使用getPerson方法才能获得Person的信息,从而获得他的Acquaintance,所以我之后改变了Node的成员以及HashMap的索引键,使用Person进行索引,这样就不必使用一次getPerson,可以直接获得Person信息,并且Person中已经实现的equals和compareTo也都是使用一次id,并不复杂,因此采用Person索引没有增加多少时间,但是这种修改减少的只是getPerson的时间,而HashMap中的getPerson根据HashCode的效果会有不同的复杂度但总体而言也不会很高,因此这种修改只是让我能够修复两个点。
第三次修复我对一个Person的遍历模式进行了修改,通过询问同学我发现返回Person的通过HashMap实现的Acquaintance的values()集合来遍历一个Person的所有连接点效率很低,因为HashMap的values并不是已经存好的一个数组,这当中我也尝试着返回HashMap的values的迭代器,entrySet,或者将其转型为ArrayList返回均失败,分别遇到了时间减少的不多,时间甚至增加,以及报错无法向下从Collection转型到ArrayList的报错这三个问题。因此最后我在Person内增加了一个ArrayList用来单独存储所有与当前Person相连接的Person,这样返回该动态数组,在遍历时就减少了很多的时间,最后通过了考察qmp的三个点并且都减少了0.x秒,也就是说刨除前两次的改进只进行第三次修复也可以完成这三个点的修复。
最后一次发现17点是考察qsl,发现我在qsl中有一点写的不够简洁。我原本的程序当id1和id2直接相连时无法通过排除点来寻找第二条路,因此我先进行了一个循环,将id1先加入已遍历的List,然后强制进入所有和id1相连的但不是id2的点,但是这时dfs的时候我每次都对List进行了clear。原本dfs一次图只需要遍历n个点,复杂度为O(n),但我现在加入与id1相连有m个点,那么我的复杂度就是O((m-1)n),多遍历了无谓的次数,因此删掉了这种情况下的List.clear,将clear放到最前面只进行一次,修复了最后一个点。
总体而言qmp的bug主要是因为我对于容器的底层实现了解的非常浅薄,qsl的问题则是在于不够仔细,对于dfs的使用也不够熟练才导致了最后的问题。
心得体会
规格的撰写由于能够使用的方法较少,为了能够写出较为完整且正确的规格就必须要将整个程序给模块化并且功能分离,如果像是第一单元自己的作业那样给自己的一个方法写规格,不仅难写并且还会非常长非常难读,丧失了规格本身的意义,因此我认为规格带来的不仅是行为上的规范,还强制你向着满足5条原则的方向靠拢。
在第二次的bug修复时我充分感受到了一份真正“面向对象”代码的好处,我仅仅是修改了容器的实现方式以及对应容器的方法修改比如由add更改为put,而其余的方法根本未受干扰,完全不需要修改,这种功能的分离让我对面向对象感觉掌握的更深了一层。
在开始的时候我也曾吐槽,有写规格的功夫代码都已经实现完了,虽然在实际情况中JML也是这样的地位,去单独撰写一份JML显得有些画蛇添足,但是在这个学校的过程中我认为还是受益匪浅,assginable让我减少了很多不必要的操作,exceptional_behavior让我充分应对异常而不是像以前只有测试抛出异常时才会想到,让我的代码更加有条理,get的时候直接调用get方法而不是去拿容器来寻找,将方法包装起来,一是简洁二是便于我们使用。
本单元的作业还让我意识到了一件事,那就是规格真的只是规格,他只是告诉你能怎么做,要做出什么效果,若是你以为是照着他的做就可以话就完蛋了。在以后书写代码时也是,在具体实现之前我们可以在心里有一个规格,这个方法能干什么有什么效果不能做什么,这样在实现时既不会手忙脚乱,这里没想到那里没想,怕这里错怕那里错,也不必担心被规格束缚手脚,尽管去实现你认为的最优解即可,出来的方法就是又符合规范又高效的代码,若是能形成这样的习惯,想必是非常棒的。