OO第三单元作业总结

OO第三单元作业总结

一、 JML语言

JML语言的理论基础

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口 规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对 方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML主要由Leavens教授在 Larch上的工作,并融入了Betrand Meyer, John Guttag等人关于Design by Contract的研究成果。近 年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不 仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满 足情况。 ——JML(Level 0)使用手册

我对JML的理解是,JML是一种对规格进行描述的语言,用户通过编写JML能够准确无二义地描述规格,程序的编写者按照JML描述的规格进行编写程序,需要确保将要实现的程序功能符合预期。另外通过JML还可以提前编写测试数据,从而将实现与测试这两个过程分离开,加快软件开发进程,提高软件开发的效率。个人感觉,JML体现了契约式设计的思想,在软件开发多人协作的时候可能更有用武之地。

  • 常用语法

    JML规范以注释的形式添加到Java代码中。当Java注释以@符号开始时,它们被解释为JML注释。也就是表单的注释

     //@ 

    or

     /*@  @*/

    基本的JML语法提供了以下关键词:

    • requires

      Defines a precondition on the method that follows.

    • ensures

      Defines a postcondition on the method that follows.

    • signals

      Defines a postcondition for when a given Exception is thrown by the method that follows.

    • signals_only

      Defines what exceptions may be thrown when the given precondition holds.

    • assignable

      Defines which fields are allowed to be assigned to by the method that follows.

    • pure

      Declares a method to be side effect free (like assignable \nothing but can also throw exceptions). Furthermore, a pure method is supposed to always either terminate normally or throw an exception.

    • invariant

      Defines an invariant property of the class.

    • loop_invariant

      Defines a loop invariant for a loop.

    • also

      Combines specification cases and can also declare that a method is inheriting specifications from its supertypes.

    • assert

      Defines a JML assertion.

    • spec_public

      Declares a protected or private variable public for specification purposes.

    Basic JML also provides the following expressions

    • \result

      An identifier for the return value of the method that follows.

    • \old()

      A modifier to refer to the value of the `` at the time of entry into a method.

    • (\forall ; ; )

      The universal quantifier.

    • (\exists ; ; )

      The existential quantifier.

    • a ==> b

      a implies b

    • a <== b

      a is implied by b

    • a <==> b

      a if and only if b

  • 工具支持

    目前有各种工具都提供基于JML注释的功能。如OpenJML,JMLUnitNG等,JMLUnitNG是一个用于jml注释的Java代码的自动化单元测试生成工具,包括使用Java 1.5+特性(如泛型、枚举类型和增强的for循环)的代码。与原始的JMLUnit一样,它使用JML断言作为测试预言。它在原始的JMLUnit的基础上进行了改进,允许对被测试类的每个方法参数使用的数据进行简单的定制,以及使用Java反射自动生成非基本类型的测试数据。

应用工具链

  • OpenJML

    使用OpenJML封装的SMT Solver方法验证,由于部分复杂规格不能验证且规格不能和方法同名,需要对规格进行修改再验证,只有修改得让工具满意了才不会出现报错信息.

  • JMLUnitNG

    选用JMLUnitNG,对第十次作业中Group接口和实现该接口的MyGroup.java文件自动生成测试文件和测试用例,运行MyGroup_JML_Test.java文件。

     java -jar D:\OpenJml\jmlunitng.jar .\MyGroup.java -d .\
     javac -cp .;D:\OpenJml\jmlunitng.jar; .\*.java
     java -jar D:\OpenJml\openjml.jar -rac .\MyGroup.java
     java -cp .;D:\OpenJml\jmlruntime.jar;D:\OpenJml\jmlunitng.jar; MyGroup_JML_Test

     

    效果如下:

     [TestNG] Running:
      Command line suite
     Passed: constructor MyGroup(-2147483648)
     Passed: constructor MyGroup(0)
     Passed: constructor MyGroup(2147483647)  
     Passed: <>.getPeopleSum()
     Passed: <>.getPeopleSum()
     Passed: <>.getRelationSum()
     Passed: <>.getRelationSum()
     Passed: <>.getRelationSum()
     Passed: <>.getValueSum()
     Passed: <>.getValueSum()
     Passed: <>.getValueSum()
     Passed: <>.hasPerson(null)
     Passed: <>.hasPerson(null)
     Passed: <>.hasPerson(null)
     Passed: <>.hashCode()
     Passed: <>.hashCode()
     Passed: <>.hashCode()
     
     ===============================================
     Command line suite
     Total tests run: 16, Failures: 1, Skips: 0
     ===============================================

     

    可以看到JMLUnitNG根据规格实现的自动化测试,实际上是针对边界条件进行的测试 ,如INT_MAX,0,NULL等等.

    我认为这个效果不够明显,实际上在进行Junit测试时,就应该根据规格对边界条件进行断言.

二、架构设计

我认为这一单元JML规格事先给出,类之间的关系其实是固定的,其实就是让我们构造一个社交网络,所以我就不贴类图了,所谓的"架构设计"主要体现在对于抽象数据和抽象方法的实现上,即重点关注数据结构的设计和算法.一个好的架构设计能够在保证正确性的前提下,具有优良的性能.

  • 第九次作业

    第九次作业相较上一单元难度看起来骤降, 只需要在基本熟悉JML语法的前提下,照着JML描述的规格写就完事了,基本上不需要考虑性能的问题.

    绝大部分的方法我都是照着规格一步步写的.

    query_circle()方法,实际上就是查询两个节点是否可达.据群友表示DFS在极端情况下可能爆栈,所以采用了BFS实现.

    由于是增量式开发,估摸着下一次大概率会考察性能,所以容器采用HashMap,以Id作为Key值,方便查找.

  • 第十次作业

    这一次增加了Group类,需要注意的新方法有getConflictSum()\getAgeMean()\getAgeVar(),

    由于对6.6s的CPU运行时间上限掉以轻心,再加上第九次作业按照规格实现没有什么大的问题,我这次沿袭了上一次的作业设计,求取ConflictSum\AgeMean\AgeVar时均严格按照规格来办,采用了双重循环.

    说真的,我非常后悔,要是当初但凡知道6.6s其实对性能仍然有一定的要求的时候,我就不会傻乎乎地照搬规格实现导致复杂度较高,遇到大量数据的时候会TLE了.

  • 第十一次作业

    这一次作业在之前的基础上,增加了money的属性,以及一些相关方法,这都问题不大,重点是query_min_path()\query_strong_linked()\query_block_sum()这三个方法。另外,还增加了从Group里删除Person的操作。有了前两次的惨痛教训,这一次我认真对待了起来,将所有重要的方法都进行了改写,主要是算法和容器使用方面进行优化,期待能得到一个好的互测成绩。

    query_min_path():

    求两个节点的最短路径,返回最短路径对应的加权长度。

    阅读了讨论区里大佬的帖子,搜集了一些博客,我采用了Dijkstra算法,并用优先队列进行优化。Java中优先队列的底层实现使用了堆。

    query_strong_linked():

    判断两个结点是否“双向联通”(除去仅有的两个点直接连接而无其他可达路径的情况),

    本来打算速成以下Targan算法,但讨论区里助教强烈暗示不用过分追求算法,这个方法不用Targan也不会超时,再加上有些同学反映实现后容易出错需要对拍,我就采用了平易近人的暴力做法,枚举每一个点判断不经过这一点的情况下是否能够连通。

    query_block_sum():

    求NerWork中连通块的个数。这个规格写的可真妙啊我一开始还没有看懂是求连通块数目。

    这里我增加了连通块集这个属性,是一个负责存储连通块的集合,在addPerson和addRelation的时候进行维护,使得isCircle和求blockSum时的复杂度为O(m)和O(1).(m为group数)

    此外,我采用了HashMap和ArrayList双容器,在查找的时候使用HashMap,遍历的时候使用ArrayList;

    在算法的具体实现时,涉及到删除插入操作且存在遍历操作时使用了LinkedList;

    容器初始化遵照了阿里手册.

    14.【推荐】 集合初始化时, 指定集合初始值大小。 说明: HashMap 使用 HashMap(int initialCapacity) 初始化。 正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1。 注意负载因子(即 loader factor) 默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值) 。 反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大, resize 需要重建 hash 表,严重影响性能。

    然而强测还是TLE了,真的太沮丧了......

三、bug分析与修复

本以为这一单元真的如助教和群里各位大佬所言,可以 好 好 休 息 了,没想到从结果来看,我这一单元却是目前效果做得最差的了.一时偷懒一时爽,开局不利,后面感觉左支右绌,无力回天.

  • 第九次作业

    群里助教和同学们都在说第三单元简单,可以好好放松一下,我却把灌水群里都是各路神仙大佬卖菜的舞台给忘了,一时得意忘形,照着规格写完连基本的测试都没有.

    结果付出了惨重的代价,这一次没有进屋,原因是isCircle中BFS没有返回值.导致该方法最终返回值均为false;但凡当时稍微测一测都能够发现.

  • 第十次作业

    这一次作业照样麻痹大意,6.6s的限制以为讨论区里各种对性能的优化,比如容器采用双容器,双重循环不行,需要缓存等等我都视而不见.当时我总认为这一单元是考察JML,6.6s对性能来说要求不高,这样在细节上的追求觉得有些没有必要.实际上我的想法完全错了!

    尽管这一次作业我进行了Junit测试,保证了程序正确性,但并没有生成极端数据或者大量数据测试是否会超时.因为双重循环,我的程序还是CPUTLE了.

    感觉这可能也和我对算法复杂度不敏感有关.

  • 第十一次作业

    虽然如上文所说的,当时我做足了优化(现在看来还不够),而且Junit测试\对拍都尝试了,进互测之前我一直以为这一次总该稳了吧?毕竟本地测的时候还是"挺快的.".....

    可是不幸的是,我还是出错在CPUTLE上,其实现在我也不知道是哪里出现了大问题,感觉我的算法复杂度满足要求,但我的程序还是慢了一点点……现在想想,应该是各方面的实现问题,如果blockSum并查集实现得好一些,数据结构再优化一下,各种方法再离散化一点,或许就能满足要求了.

    如果说,第九次\第十次作业更多的是后悔,后悔为什么没有充分测试,为什么没有听取讨论区的意见改成缓存处理,第十一次作业更多的是无奈,我认为我各方面都进行了优化,理论上,应该没什么大问题了,复杂度应该不会太高.当初看到中测限制是6.6s,以为要求放宽松了,于是在0-2.5s之内我就认为满足要求了,没有再继续搞.可是现在想想,就算当初我认为要求是2s,我好像也有点不知道该继续怎么优化的无奈了,觉得最低要求是2s,这个难度对我来说真的有点大.

    然而我了解到很多人一般都是1s左右,我的程序一定哪里有问题.

    Hack他人情况:

    第十次作业我主要是Junit测试,发现Junit测试主要是检查程序的正确性,没有hack到人;

    第十一次作业我利用极端数据,如大量qsl\qbs\qmp来测程序是否会发生超时,共hack到了8个bug.

四、心得体会与反思总结

  • 心得体会

    JML单元体验很不好,说实话我感觉并没有学到什么东西.这一单元甚至一开始都没有进互测,这让我非常沮丧.

    最初我认为JML这一单元是以JML语言为媒介,让同学们熟悉规格化设计的思想,能够在编写代码和测试时都遵从规格化设计.本以为这单元的重点是规格化,没想到一单元下来,实际上难度居然在于数据结构和算法?除了第九次作业大意失荆州之外,其他的bug都是CPUTLE.所以我真的很不明白,明明是JML的考察,为什么最后落脚点和难点是追求降低算法复杂度上?

    当然我好像也没有资格在这质疑,毕竟这一单元我完成得并不咋样.只是,这样重心放在算法而不是JML规格化设计的教学要求让我也对JML的实际作用大为怀疑.从JML的工具链来看,感觉到JML好像并没有很成熟?另外,这一单元本质上是图,规格已经写得很复杂了,如果遇到更复杂的java程序,JML应该怎么清晰地表达呢?之前我对于JML可以使测试和实现分离的期待,在实际使用过程中似乎没有得到充分地验证......

  • 反思总结

    这一单元的滑铁卢主要有以下因素:

    客观上我能力不足,这一单元注重对代码的测试,想要强测满分对程序的算法复杂度也有一定的要求.而,程序测试一直是我不擅长的, 数据结构和算法的基础不牢固.因此,虽然通过Junit测试我能检验程序的正确性,但是我不会有针对性地生成极端数据(指测试是否超时的大量指令),对算法复杂度掌握不够,只会对算法复杂度有个大概的评估,而不是准确了解(本以为大致评估复杂度就已经够了,可以现在看来容易造成认为已经满足要求的错觉).

    主观上我掉以轻心,甚至态度上有些敷衍. 每一单元我都应该认真对待,向其他同学学习,在各个方面做到个人的极致,追求完美而不是留有遗憾.事实证明只有尽力而为才不会有那么多的遗憾.

    希望下一单元能够迎接更好的自己 !

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