面向对象程序设计第三单元总结

面向对象程序设计第三单元总结

实现规格所采取的设计策略

  1. 对规格的理解:

    第三单元学习的JML(Java Modeling Language)是一种用于Java模块的行为接口规范语言。JML提供了语义来正式描述Java模块的行为,防止了关于模块设计者意图的歧义,而这就是规格。一般是以注释的形式加到Java代码的前面,并且一般都是以@符号开头。

    // @ JML 规格
    
    /*@ JML 规格
      @*/
    
  2. 基本的JML规格语法

    • require A

      定义前置条件A

    • ensure A

      定义后置条件A

    • signals A

      定义后面抛出给定异常时的后置条件A

    • signal_only A

      定义当给定的前置条件A存在时可能引发的异常。

    • assignable A

      定义可以赋值的对象A

    • pure

      指不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。

    • invariant A

      定义不变式A, 并且要求在所有可见状态下都必须满足特性A

    • loop invariant A

      定义循环不变式子

    • also

      在子类重写父类方法需要补充的规格

      链接方法中设计的多个规格(如正常规格和异常规格)

    • assert

      定义JML中的断言

    • spec_public

      为规范目的将受保护的或私有变量声明为public。

  3. 设计策略

    • 从整体到局部: 在写代码之前我都会对将要实现函数的规格整体读一遍,搞明白每个函数需要实现的功能和限制条件。对作业整体功能有一定了解之后定义变量和实现方法。
    • 增量式开发:三次作业实现的是增量式开发,所以每次都是在上一次的基础上增加功能或者改变功能。这两个部分都是通过两次作业的规格文本对比进行的。
    • 从约束条件入手:对于每个方法,都是先写异常处理,后写功能函数。

基于JML规格来设计测试的方法和策略

  1. 测试工具:JmlUnitNG, OpenJML, C++

  2. 测试过程:

    1. 首先了解了openjml的基本用法

      • 利用OpenJML对JML注释的完整性进行检查
      java -jar openjml.jar [-check] files
      
      • 对程序代码进行静态检查
      java -jar openjml.jar [-esc] files
      
      • 对程序进行运行时检查
      java -jar openjml.jar [-rac] files
      
    2. 利用JmlUnitNG 生成和运行单元测试

      • 运行 JMLUnitNG ( ) 来生成测试类
      java -jar jmluniting.jar files
      
      • 修改测试数据生成策略以添加自定义测试数据
      • 用openjml来编译被测类
      java -jar openjml.jar [-d] bin/ [-rac] files
      
      • 进行测试
    3. 第一次测试结果

      过程中会有很多奇奇怪怪的错误和警告, 而且感觉测试数据不够随机,总体测试效果不好

    4. 改变测试策略

      首先利用c++建立一个数据生成器, 在保证数据有一定强度的情况下进行验证, 总体效果比较好。

容器选择和使用的经验

  1. 本单元使用到的容器: HashMap, LinkedHashMap, Deque, PriorityQueue
  2. 使用HashMap的原因: group, message, person 都有着独一无二的id, 抓住了这一关键点,可以联想到Map的键值对, 而HashMap是基于哈希表的 Map 接口实现,查询和插入更具优势。
  3. 在实现并查集的方面也用了Hashmap。Hashmap 本生就是两个数据的映射关系,天生就具有并查集的特点,而且 Hashmap 可以支持负数。
  4. Deque是双端队列,在sendmessage中有一步是像person2的message队列的首部插入一条message,利用deque可以轻松做到这一点,当然也可以用数组模拟(向后插入,但把后面当成头部),但deque支持的头尾都能进行插入的操作在功能拓展上有它的独到之处(虽然作业中并没有)
  5. PriorityQueue是在第三次作业对Dijkstra进行了堆优化的时候用到

性能问题分析

第一次作业

第一次作业中主要涉及到的就是isCircle()函数和queryBlockSum()两个函数,第一次写的时候没有真正读懂这两个函数的规格,就按照规格模拟,规格如下:

/*@ public normal_behavior
      @ requires contains(id1) && contains(id2);
      @ ensures \result == (\exists Person[] array; array.length >= 2; 
      @                     array[0].equals(getPerson(id1)) && 
      @                     array[array.length - 1].equals(getPerson(id2)) &&
      @                      (\forall int i; 0 <= i && i < array.length - 1; 
      @                      array[i].isLinked(array[i + 1]) == true));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id1);
      @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
      @*/
    public /*@pure@*/ boolean isCircle(int id1, int id2) throws PersonIdNotFoundException;

    /*@ ensures \result == 
      @         (\sum int i; 0 <= i && i < people.length && 
      @         (\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId()));
      @         1);
      @*/
    public /*@pure@*/ int queryBlockSum();

第一遍想到的是只要id1和id2之间存在一条路径就把id1和id2认定为isCircle, 并且写了个时间复杂度很高的bfs, 后期在测试的时候发现对于大一点的样例运行时间超级长,一直在TLE的边缘徘徊,后又仔细再次阅读函数规格,queryBlockSum()函数就是在求图中存在的连通块个数,然后茅塞顿开,连带着将isCircle()的实现改为并查集,也加上了按秩合并,再次测试后发现运行时间是原来的\(1/20\)

所以还是要看清规格

第二次作业

Group类中getValueSum()函数,如果直接暴力实现的话时间复杂度是\(O(n^3)\)的,那么有很大概率被卡TLE, 所以在Group中维护了一个valueSum和ageSum, 只在addPerson, deletePerson, addValueSum的时候对其进行修改,这样可以避免每次询问都重复计算的问题,也大大降低了复杂度

Group中的getAgeVar()函数,这个函数式用来求方差的函数, 我们知道方差如果按照正常计算的话会是\(O(n)\)的,而且如果不预先处理平均值mean的话复杂度可能会到\(O(n^2)\), 而方差是可以利用公式直接\(O(1)\)求出来的,当然要对其中的一些值进行维护,这也算需要注意性能问题的地方。

第三次作业

第三次作业也是平平无奇,唯一有性能问题的地方应该就是求最短路的部分 (deleteColdEmoji部分用迭代器和lambda表达式删除给程序运行时间的影响感觉没有大到能TLE的程度)

对于最短路,我们可以想到几种经典算法,Floyd, SPFA, Dijkstra

而在这里当然是用稳定的Dijkstra了

而Dijkstra的不同实现方式的时间复杂度也有很大区别

设点数为\(n\)边数为\(m\)

那么对于暴力遍历的方法的时间复杂度为\(O(n^2 + m)\)

利用小顶堆优化之后复杂度为\(O((m+n)log(n))\)

利用斐波那契堆优化之后时间复杂度为\(O(nlog(n) + m)\)

(节点的个数\(n\)一般是要比边的条数\(m\)少)

所以追求性能的话可以利用斐波那契堆对Dijkstra进行优化。

但是由于这次作业给了\(6s\)的时间,直接用优先队列PriorityQueue写小顶堆也不会TLE。

作业架构设计

三次作业几乎都是依照这JML规格实现,只有很少的地方利用了自己的语言实现规格

第一次作业

class OCavg OCmax WMC
Main 1.0 1.0 1.0
myexceptions.MyEqualPersonIdException 1.5 2.0 3.0
myexceptions.MyEqualRelationException 3.0 4.0 6.0
myexceptions.MyPersonIdNotFoundException 1.5 2.0 3.0
myexceptions.MyRelationNotFoundException 2.5 3.0 5.0
MyNetwork 2.7 6.0 29.0
MyPerson 1.5 4.0 17.0
Total 74.0
Average 2.1 3.1 10.5

第一次作业的Network类的WMC比较高,一方面是由于Network中有很多判断语句,另一方面是由于第一次实现isCircle()函数的时候利用了bfs

第一次作业主要改变就是再实现人与人相识的地方用来并查集来实现。

第二次作业

class OCavg OCmax WMC
Main 1.0 1.0 1.0
myexceptions.MyEqualGroupIdException 1.5 2.0 3.0
myexceptions.MyEqualMessageIdException 1.5 2.0 3.0
myexceptions.MyEqualPersonIdException 1.5 2.0 3.0
myexceptions.MyEqualRelationException 3.0 4.0 6.0
myexceptions.MyGroupIdNotFoundException 1.5 2.0 3.0
myexceptions.MyMessageIdNotFoundException 1.5 2.0 3.0
myexceptions.MyPersonIdNotFoundException 1.5 2.0 3.0
myexceptions.MyRelationNotFoundException 2.5 3.0 5.0
MyGroup 1.6 3.0 21.0
MyMessage 1.0 1.0 8.0
MyNetwork 2.5 6.0 71.0
MyPerson 1.4 3.0 21.0
Total 151.0
Average 1.8 2.5 11.6

第三次作业

class OCavg OCmax WMC
Edge 1.0 1.0 4.0
Main 1.0 1.0 1.0
MyEmojiMessage 1.0 1.0 9.0
MyGroup 1.6 3.0 21.0
MyMessage 1.1 2.0 10.0
MyNetwork 2.8 14.0 101.0
MyNoticeMessage 1.0 1.0 9.0
MyPerson 1.3 3.0 23.0
MyRedEnvelopeMessage 1.0 1.0 9.0
Total 187.0
Average 1.7 3.0 20.7

第三次作业进行的架构改变

由于人与人之间的关系可以看成无向图,每个人有着自己认识的人(包括自己),所以用到了HashMap存储每个人认识的人的id和value

而acq就是每个人储存的邻接表,再Dijkstra中可以对邻接表进行遍历,而不必分别对认识人的id和value分开的进行遍历(改变了之前用下表进行遍历, 改成了利用迭代器进行遍历)

此外新开了一个类Edge用于保存边的相关属性, to和cost, 并且再Edge中重写了compareTo() 这样处理的PriorityQueue<Edge> 就是一个小顶堆了。

Hack过程与结果

第一次作业

感觉没有同学意识到可以利用并查集来解决isCircle()问题,至少我房间里大多都是写的dfs, 而且有一个人写了并查集但貌似是假算法,复杂度高到可以把他卡掉。

第一次作业Hack到了2个人,两个人都是TLE。

第二次作业

这次貌似所有人都意识到了并查集的妙处,全都改成了并查集,但是还是有人写假算法,被卡TLE了,还有一个人在处理函数的异常上没把限定条件判对从而WA了

第三次作业

和平的一次作业,没有人被Hack,应该是给的\(6s\)时间太长了,不能构造出数据卡T。也许\(4s\)可以,虽然几乎大家都进行了堆优化,但是还是存在假算法的情况,时间复杂度比较高。

自己作业情况

三次作业中的强测和互测都没有被Hack到。

感悟与总结

第三单元作业的总体难度并不大,坑点也不多,感觉这一单元的主要目的在于接触JML,对Java的规范化编程和验证进行一定了了解。的确感觉到JML语言可以严谨的描述Java模块的功能。同样我们可以反过来验证Java代码是否符合规格定义,从而达到形式化验证的作业,使得Java程序的正确性得到保证。

总体感觉这个单元比较轻松,但的确有所收获,并且对规范化编程有了更多的认识。

posted @ 2021-05-30 01:31  QuantumBolt  阅读(58)  评论(1编辑  收藏  举报