0 引语
在第一单元中,我们初步了解了java语言和面向对象的思想。紧随其后的第二单元,我们学习了多线程的相关知识。讲道理,与面向对象思想相关的知识我们已经掌握了,所以这一单元我们学习的是一种新的工具链:JML。在本单元的学习中,我们依旧是通过迭代开发的方法,完成了三次作业。这三次作业的目标是完成一个小型社交关系模拟系统,例如,可以使用add_person添加人,在两个人之间可以添加关系等,使用一些指令可以询问当前关系统计、检查状态等,我们要做的,就是在每一次输入指令之后给出对应的输出信息。
嗯嗯,从判断正确性的角度看,比上一单元简单多了。毕竟不是多线程,既不用考虑线程安全问题,也不用考虑输出结果的正确性检验。在互测之中,也只需要简单的对拍就ok了,看起来还挺和蔼可亲的……
桥豆麻袋,JML?那是个什么东西?
一、JML简单梳理及实例分析
1.1 JML是什么?
JML的大名是Java Modeling Language,即Java统一建模语言,它以模式化的方式,描述了对于一个类或方法而言,应当满足什么条件,在执行后应该满足怎样的结果。但是,说实话,我并不喜欢这种描述语言,总觉得有些地方是在“不说人话”,可能是我还没有领悟到JML的精髓吧……虽然我知道能够用OpenJML等工具对方法进行是否符合JML正确性的检测,但这东西用起来实在麻烦,所以我最终就没有使用。
嗯……写出求个最短路的JML需要这么多东西,这还不算exception behavior的部分,还好不是让我写,不然我人就没了。即使如此,在本单元的实验课中还是没能躲过去写JML的考验,然而我完成的并不是很好,有一说一,比起算法的设计,单独写这东西反而更令人头秃。【挺秃然的.jpg】
1.2 JML实例分析
无论如何,我们的JML之路还得继续。下面我来用一个实例来说明一下JML是如何对方法的行为进行定义的:
以addPerson举例,这是我们作业中的第一个指令所对应的函数,也是一切的开始,它的jml如下:
@ public normal_behavior
@ requires !(exists int i; 0 <= i && i < people.length; people[i].equals(person));
@ assignable people, money;
@ ensures people.length == old(people.length) + 1;
@ ensures money.length == people.length;
@ ensures (forall int i; 0 <= i && i < old(people.length);
@ (exists int j; 0 <= j && j < people.length;
@ people[j] == old(people[i]) && money[j] == old(money[i])));
@ ensures (exists int i; 0 <= i && i < people.length; people[i] == person && money[i] == 0);
@ also
@ public exceptional_behavior
@ signals (EqualPersonIdException e) (exists int i; 0 <= i && i < people.length;
@ people[i].equals(person));
首先,它定义了两个行为:正常行为和异常行为。先来看相对简单的异常行为:当存在一个取值在[0,people.length)范围内的i,使得people[i]和person的值相等,则程序会抛出一个EqualPersonIdException异常。也就是说,当目前的people数组中存在和person相等的人时,就会满足此条件,并抛出异常。至于EqualPersonIdException具体是什么,应怎样处理,这些都不归我们管了。然后是相对复杂的正常行为部分。可以注意到,它的requires部分正好和异常行为的条件相反,也就是说,我们的程序不会再有这两种行为之外的任何行为了,这也能证明这个jml写得比较完善,涵盖了所有出现的情况。某需要判断length<1111的函数表示强烈谴责。
那么,需要我们“确保”的又是什么内容呢?
第一,people的长度等于旧的people长度加1。
第二,money的长度等于people的长度(第三次作业新增)。
第三,对于所有的“旧”范围之内的i,都存在一个j使得对应的people和money数值相等。
第四,在“新”范围内存在一个i,使得people[i]是新增的person并且money[i]为0。
说人话,实际上就是人员个数加一,钱跟着人员走,旧的都不能变,把新的放上去这样的要求。所以,只要读懂了JML,这个函数该做什么就一目了然了。
二、JUnit单元测试检查
2.1 这东西好难用
尽管我们在上课中提到可以使用JUnit进行单元测试,分享课中也有同学分享过相关的内容,但我感觉它用起来还是过于复杂了。在三次作业中,我都没有使用Junit进行测试,所有的测试都是像前两单元那样构造数据去看结果的,在数据生成器的狂轰滥炸下,程序的正确性已经得到了保证(然而第二次作业在效率上还是翻车了)
大概是为了强制让同学们使用JUnit进行测试吧,在本单元的第二次实验课中,我们通过“垃圾回收机制”练习,要求提交的代码必须包含JUnit测试,这就让人很难办了,于是,我直到那一晚才开始恶补JUnit知识,然后才稍~微的会用那么一丢丢。
但是,这东西用起来就发现,它看上去难,用起来……更难!我真的想在这里吐槽一下JUnit,虽然它的用途是单元测试,可是多数时候我们难以想到完备的测试集(你想到了的情况错不了,错了的情况你想不到),况且很多时候,一个类和其它类有千丝万缕的联系,这种情况下我们更不好利用JUnit进行测试。编写JUnit测试往往要花费大量的时间,而这些时间本来可以用在更详细的debug上。而且,JUnit测试难以测出程序的效率问题。
那么,这么难用的东西,我们为什么还要使用它呢?
那就不用!
你以为我要欲扬先抑?NO,测试程序的方法有很多种,干嘛偏要用难用的这一种呢?我大实话就放在这里,JUnit的测试这部分,如果不是为了得分,我早就把它扬了。
2.2 JUnit测试实例
这次的程序我从一开始就没用JUnit,为已经完成的程序编写测试没有意义,所以我就拿之前做过测试的实验题来举例子。
在检测MyHeap的add方法时,我在MyHeapTest中是这样写的,首先输出“ready”的信息,然后重复进行10次添加人的操作,每次添加后,检测myHeap的大小是否比原来多1,并且判断是否已经存在这个新的人,如果不存在,那么assert就会报错。实际上,我这里缺少了对于Heap中人员顺序的判断,如果要测试的更完整一些,应该再加几个assert。程序运行的结果如下:
顺带一提,这里Before Method和After Method并不在这个函数里,它们在setUp()和tearDown()中。
三、程序架构分析与迭代历程
3.1 第一次作业
第一次作业方法复杂度分析(官方包未列出):
第一次作业类复杂度分析:
第一次作业其实还好啦还好啦,基本没有几个被标红的部分,因为我当时是几乎照搬了JML上的定义,JML让我用静态数组我就用静态数组,所以也没什么好说的。事实证明完全照着JML写虽然能够完成任务,但是程序的效率就难以保证了。
3.2 第二次作业
第二次作业方法复杂度分析:
第二次作业类复杂度分析:
不得不说官方接口是真的香,每一个函数都是完成一个很小的功能,使你的函数不再复杂。注意到类似addToGroup这样的函数还是被标红了,不过这东西本身就要处理很多情况,所以就不说什么了。但是!从这次作业开始,千万不要照搬JML的写法!这会使你的程序极其容易出现超时等问题!我这次翻车就是翻在了这里。关于我翻车的经历,请见第四部分。
3.3 第三次作业
第三次作业方法复杂度分析:
第三次作业类复杂度分析:
第三次作业很多东西在算法上就已经比较复杂,对于query_strong_link这个函数,我新建一个类QslChecker来专门完成它的检测,另外,本次作业我使用了HashMap代替原来的静态数组,HashMap在查找等方面都非常好用!同时,也为了HashMap使用便利的需要,搞了一个Link类。这一次的作业吸取了上一次作业翻车的教训,取得了很好的结果。话说你家Runner标红真的不用管管吗(滑稽)
四、强测、互测与bug修复
4.1 我的翻车历程
很遗憾。我在本单元的第二次作业中,因为过于大意而轻敌了,最终导致了翻车惨剧的发生。接下来我就来讲述一下这段悲惨的经历……
这是第二次课的数据限制,人最多有5000个,qnr指令最多只有333条,何况,这个qnr还是上次作业的指令,我就没把它当回事。反正是以前的指令,而且迭代的时候也没发生任何变化,这个函数不用改也行吧。当时我就觉得啊,三百条指令,你能秒杀我?你今天能三百条指令把我袁劭涵秒了,我当场就把这个强测作业吃掉!
然而,从一进入互测起,我就觉得不对劲:怎么我随手交一个数据就能hack到人,甚至一串五?难道……
果然,强测一出来,BOOM!翻 大 车 了
我做了这么多次作业,身经百战见得多了,虽然也有强测出错和互测被疯狂hack的情况,可是强测炸成仅剩20分还是头一次见。悲伤之余,我痛定思痛,开始研究我究竟是哪里出了问题。最后终于发现了:我当时太照着JML写,JML怎么说我就怎么做,JML曾经曰过:
它在qnr里用到了compare_name这个函数,我也是照着做了,但是,问题就出在这里,cn里面其实是重复判断了id1和id2是否包含在人员列表内,我用的是静态数组,所以contains本身的时间复杂度是O(n),进而qnr的复杂度就悄悄的变成了O(n²)了,然后就超时了。
这个故事告诉我们,千万不要照着JML写代码!千万不要照着JML写代码!千万不要照着JML写代码!在需要讲究效率的时候,一定要自己构造合适的数据结构!
4.2 互测bug知多少
那啥,关于互测嘛……我发现,现在同学们逐渐变得佛系了,互测的时候打人的次数大大减少,不像以前各种naive满天飞了。
第一次作业,大家和平共处,平安无事。(其实是因为第一次作业真的没什么bug可找,对了就是对了)
第二次作业,这就要值得说说了,由于我强测的翻车,我不慎跌入了C屋,在这样的屋子中几乎是随便交数据就能hack到人,我自然也是杀疯了,最后似乎在bug修复环节赚到了十几分,然而尽管如此还是无法弥补强测足足80分的损失,所谓“战术C屋”是不存在的。C屋一个bug才一分真坑爹
第三次作业,我又回到了A屋,重振雄风!然后,见到的自然也是不怎么hack的大家们。我和另一位同学各自hack出了别人的一个bug,我hack出的是有人的borrow_from写错了,把加号写成了减号(大家的钱都越借越少最后都成了负翁),最后当然也是小小的赚了3分,另一位同学hack出的是有人把dfg写错了。其实我的代码也有个bug,只是你们没hack出来(
话说,A屋互测是正和游戏,建议大家能打就尽量打打,说不定就有惊喜。C屋是零和,总感觉我打他们之后有点损人利己的嫌疑(
五、分享课PPT截图分享
最后还是要提一下,我在本单元的分享课中分享了《第三单元算法讲解和技巧分享》,你以为这单元是JML,其实这单元是算法哒!由于PPT似乎不能上传到这里,再讲一遍也不太现实,这里我就放出几张PPT截图,感兴趣的同学可以自行了解。