OO第三单元总结
- OO第三单元总结
- 一、JML理论总结
- 表达式
- 方法规格
- 类型规格
- 二、应用工具链
- 三、JMLUnitNG部署
- 四、架构分析
- 第一次作业
- 第二次作业
- 第三次作业
- 五、bug分析与修复
- 六、心得体会
- 一、JML理论总结
本单元的内容是根据课程组给出的JML规格完成相应代码,整体项目主要是构建一个复杂的网络系统。总的来说功能实现不是主要难点,更加考察在实现JML规能的同时进行算法的优化。
一、JML理论总结
JML是对一种java的规格描述语言,以注释的形式写在java代码块中,用于描述类中各个方法的行为,其具有无二义性于,因此在一个大工程项目中便于团队之间的交流,避免文字注释出现的歧义。同时JML只关心功能,不关心具体实现,方便后续由程序自动对代码的正确性进行测试,这在一个大工程中也显得尤为重要。
JML语言的要素大值由三个:表达式,方法规格,类型规格。
表达式
表达式包括原子表达式,量化表达式,集合表达式,操作符等。这是JML中最核心的部分。
原子表达式中包括:\result, \old, \not_assigned等等,多是对单个的非容器型对象的描述。
量化表达式:\forall, \exisits, \sum等等,通常用于表达对一个集合的各种操作。
集合表达式:表示对集合本身属性的一些描述与限制。
操作符:多用于逻辑关系表达,如且、或、等价等等。
方法规格
方法规格就是对一个方法功能具体的描述,其核心有三个要素:前置条件,后置条件,副作用,异常。
前置条件:指使用该方法所必须满足的一些前提条件,不具备该条件则不能调用该方法。
后置条件:该方法要运行结束后要满足的条件。
副作用:该方法运行完成后,哪些成员属性改变了。如果为\nothing则表示不允许对任何成员进行修改。
异常:方法行为的一种,当满足一定条件时,要求方法抛出相应异常。
从中可以看出,JML不关心方法的实现过程,只关心条件和结果。
类型规格
类型规格是对java中数据变量的约束,它不直接出现在方法规格中,但所有的方法规格都必须满足类型规格。
二、应用工具链
- Junit:最常用的工具,可以方便的对某个方法生成测试代码的模板,必须自己编写测试用例即可轻松实现单元测试。
- OpenJML:对JML规格语法的检查。
- JMLUnitNG:通过JML规格,对类自动生成测试用例。
三、JMLUnitNG部署
JML的配置确实让人太抓狂,先是无论如何无法运行,后经大佬指点,将JDK版本切换为1.8,终于能够把JMLUnitNG运行起来了,但是第一次运行跳出不下白条警告信息和error,ok,众所周知,warning等于没有error,直接跳过。来看error,怎奈error信息里居然充满”马赛克“?,零星的几个单词字母被”马赛克“分割构不成完整的意义。想了一想,由于我是git bash运行的,bash可能信息编码格式不支持,于是改为了windows自带的cmd,终于呈现出了完整的信息,修改了一些由于语法格式的不支持,最终运行成功!
以下为对MyGroup()
的自动测试结果
MyGroup()
类中多是一些pure的get方法,以及一些对成员变量的add操作,从add的数据可以看出,测试对如0这类边界数据有一个良好的测试,但对于图的复杂操作测试效果不明。
四、架构分析
第一次作业
UML图
第一次作业架构非常简单,或者说完全没有自己思考架构,由于也没有任何性能上的要求,我直接把所有的方法JML规格”照抄“了一遍,顺利通过。存储数据的容器全部Arraylist。
唯一一个难点是isCircle判断可达性,为防止爆栈,采用BFS算法。
第二次作业
UML图
第二次作业增加了一个Group类,并且限制了CPU的运行时间,由于指令数巨大,如果每次查询仍采用遍历操作势必超时,因此在第一次作业的基础上给所有需要遍历Arraylist来查询的容器添加了一个对应的HashMap容器,这样将查询的复杂度成功降至O(1)。
其次,Group中涉及对Group中所有成员的年龄加和,年龄平均,年龄方差的计算,同样如果每次都遍历进行加和时间复杂度高,很可能造成超时,因此我选择采用了一个缓存结构,在Group中增加ageSquareSum,ageSum, valueSum, relationSum成员变量,分别表示年龄的平方和,和,value和,relation和。每次向Group进行add操作的时候即更新这几个变量的值,进行查询操作时只需更新这几个量就行,年龄的方差采用公式((ageSquareSum - 2 * mean * ageSum + n * mean * mean) / n)来计算。
不过有一点值得注意,不仅时addPerson的时候需要更新Group中的值,在两个Person 进行addRelation操作时,与二者相关的所有Group的value和relationSum都有可能发生变化,因此每个Person必须知道自己处于哪些Group中,每次addRelation时由Person将所有相关Group的值更新。
第三次作业
这是本单元第三次作业的UML图
整体架构较第3次作业没什么变化,增加了一个Edge类保存Network每一条边的信息用于新增加的各种搜索操作。第三次作业新增的几个方法计算最短路径,查找双连通分量,计算连通块都涉及对整个Network的遍历,甚至是多次遍历,每一个复杂度都极高,不出所料,最后强测超时了1个点。
三个最难的方法,寻找最短路径采用了堆优化Dijkstra算法,如果采用朴素的Dijkstra算法时间上限可达1×109,而堆优化能将这个值降到小于2.5×107。
双连通分量的查找我采用了暴力枚举删点+bfs进行搜索的算法,更好的方法应该是使用tarjan算法找出割点,再对割点进行删除验证,但是tarjan算法本身不简单,之前从未接触过,感觉大概率出现bug,产生功能性错误,因此基于优先保证正确性的考虑,我并未选择tarjan算法。
连通块数量计算我算用了bfs遍历整个图的算法,统计最终的bfs次数,暴力算法,但也只需把整个图遍历一遍,总体可以接受。如果在前面的架构中采用了并查集那么该方法会异常简单,再此不再赘述。
五、bug分析与修复
第一二次作业强测互测都未查出bug,我在课下使用junit包括自动生成的测试样例进行了大量测试,正确性得以保证。同时大量采取了缓存操作,便利与查询通过不同的容器进行,时间复杂度也非常安全。
第三次作业在正确性上也并未发现任何bug,但在强测中一个点出现了超时,如前面所说,在查找双连通分量的时候采用了暴力的算法,导致了超时,目前正在尝试是否能通过一定的改进缩短运行时间。
整体hack的测略就是把其他同学的代码扔进自动评测机,对比输出答案。
六、心得体会
在这一单元的学习中,我首次接触了JML这种形式的语言,其目的在于方便程序员间的交流,也便于根据规格自动化对代码进行测试,这在一个复杂的大型工程中意义重大。在第一次作业的时候我还比较轻视JML,认为这个规格不是相当于把大部分代码都给出来了,并且JML写起来比代码还有难,有点没必要的意思在里面,我也以开课以来最快的速度完成了第一次的作业。但到后面两次作业我爱才渐渐意识到JML真正的意义,JML只是规定了条件和结果,具体的实现优化都要靠程序员自己,如果把JML直接”抄一遍“确实也能确保程序的正确性,但是性能上面直接死亡,实际上老师也说实际工作中优化的工作才是难题,功能的正确性是基本中的基本。