BUAA_OO 第三单元总结

一、JML理论基础、相关工具链

JML理论基础

JML(Java Modeling Language)是一种形式化的、面向Java的行为接口规格语言。它使用了Javadoc的注释方式来对程序设计过程中的数据、方法等进行契约式的约束。

在实际的开发过程中,程序在实现之前需要一系列严谨的设计,而程序需要严格按照设计来进行开发,而JML正好弥补了自然语言不够严谨的缺点,通过使用离散数学中数理逻辑系统,JML独特的语法,在程序中写成注释的形式,对方法和数据进行要求和约束。在我的理解里,如果把实现一个程序比作建造一座建筑,那么JML就好比是这座建筑的图纸,虽然不像自然语言一样对任何人来说都通俗易懂,但是对于专业人士来说它代表着严谨。

设计的重要性不言而喻,而JML的方法规格中通过使用前置条件(pre-condition)、后置条件(post-condition)、副作用副作用范围限定(side-effects)来对一个方法进行限定,这样的方法就将需求很好的转化为清晰明了的设计语言,这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。

相关工具链

近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。

  • OpenJML:用来检查JML是否规范,符合语法
  • JMLUnitNG/JMLUnit:用来自动生成测试用例

二、部署JMLUnitNG

装好了jdk8之后还是NoSuchField...

三、架构设计

由于课程组已经给出了几乎全部方法的JML规格,在完成功能性上的难度相比于前两个单元小了很多

第一次作业

按照课程组下发的带有JML的官方接口,需要实现的只有Person和Network接口

这次的强测数据量很小,所以也没有牵扯到使用什么效率很高的算法之类的,只需要按照JML规格来一步步实现就可以了,目的大概是熟悉JML,为后两次作业作准备。

Bug分析

整个程序写完就直接提交并且一次AC,加上这次作业真的没什么难度,所以天真地认为一次写出来一个完美的程序,直到周日中午12点才渐渐感到事情并不是这么简单。

最终发现自己强测全部WA,而原因正是由isCircle()方法中的DFS算法忘记排除已经遍历过的人,导致了死循环,这才意识到中测等于没测

此外还发现一个强测并没有测出来的bug,是isLinked()方法的问题,并没有直接判断id1=id2时的情况,但是由于后面的方法中我都没有用到isLinked()方法,故也没有太注意。

第一次作业让我意识到的一个大问题是在阅读JML规格的时候经常会出现嵌套很多层括号的情况,这在后面的两次作业中都差点让我栽了跟头,并且前文提到的关于isLinked()方法的bug也是由这个问题所引起的。

第二次作业

第二次作业还是在第一次作业的基础上进行迭代开发,增加了一个对Person分组的一个Group接口,这次的重点在于强测中每个测试点都由10w条指令构成,并且限时6.6s。若一不小心没有注意复杂度便会造成一片的CTLE惨案。

其实涉及到需要控制复杂度的方法只有Group中的queryRelationSum()queryValueSum()方法。这两个方法的JML规格中采用了两重循环的设计,达到了O(n2​)。我的解决方案也很简单就是利用缓存,将relationSum和valueSum定义在MyGroup中,在进行改动时要分为两种情况:

  1. 先加人,再加relation

    在addRelation时判断id1和id2所对应的人是否都属于同一个Group,若是,就将\(relationSum+=2\)\(valueSum+=value*2\)

  2. 先加relation,再加人

    再addToGroup时判断即将加入Group的人是否与组内的一个或多个人有关系,若有,就将\(relationSum+=2\)\(valueSum+=value*2\),并且再最后加上一句\(relationSum++\),因为每个人都是isLinked自身

还有一个要注意的地方就是在算组内成员的平均年龄和方差的时候要注意JML的括号,否则会因为时int类型的除法造成精度损失。

其他部分的设计都与第一次作业相同。

Bug分析

时隔一个月(从上一单元的最后一次作业开始)终于又进入了互测room,room内其他人大开杀戒的同时并没有波及到我。但是强测却有一个点出现了CTLE,应该还是时间复杂度的问题。

第三次作业

这次作业虽然只要求在2s内运行完成3000条指令,但是新加入的一些方法涉及到图论中的一些经典算法,只要稍加注意的话还是问题不大。

我在实现代码的过程中首先对第一次作业就出现问题的isCircle()方法进行优化,借鉴了一部分并查集算法的思想。具体实现就是在MyPerson类中增加一个int类型的crowd属性,用来记录每个人所属的块的编号,简而言之就是如果甲和乙之间有relation,那么甲、乙、所有甲的acquaintance、所有乙的acquaintance的这些人的crowd都应该相同,这样在isCircle()的判断中只需要判断两个人的crowd编号是否相同即可,与原来的dfs遍历简化了不少。

其次就是加入了queryMinPath()方法,需要寻找最短路径,由于人与人之间的value都为非负,故一下子就想到了上学期学过的Dijkstra算法,但是传统的Dijkstra算法的复杂度是O(n2),很有可能在强测中超时,再加上Java有内置基于小顶堆的优先队列,可以将算法的时间复杂度优化为O((m+n)logn)。

还有一个稍微复杂一点的方法就是queryStrongLinked(),需要查看两个Person之间是否双连通,借助Tarjan算法即可完成。

对第二次作业进行的优化还有一个方面就是对所有的容器进行了初始化操作,这样虽然牺牲了一些空间,但是没有了扩容操作,大大减少了运行时间。

Bug分析

和上一次作业一样,互测什么都没有发生,强测因为CTLE挂了一个点。

算法方面能够优化的空间不是很大了,刚开始我想放弃修复了,但是在周三的讨论课上听到同学分享时说到由于Person对象太多,对储存Person的容器初始化后反而会造成运行时间的增加。抱着试试的心态将所有储存的容器初始化删除后就过了所有的测试点。

四、心得体会

  1. 在依照规格实现代码的过程中逐渐体会到了设计在程序开发过程中的重要性
  2. 在有了初步的设计之后,保证功能性的正确的难度并不大,真正的挑战在于性能上的要求
  3. 虽然说代码要严格按照规格来实现,但是还是要先读懂规格,明白此方法要做的任务,具体的实现方式并不一定要完全由规格来照搬

你可能感兴趣的:(BUAA_OO 第三单元总结)