面向对象设计与构造第三单元作业总结

面向对象设计与构造第三单元作业总结

本单元我们主要学习了jml语言的书写以及根据jml语言编写代码;根据作业的背景,我们还复习了图论的相关知识,并学习了新的相关算法。本单元总体来说是对我们代码设计以及代码测试方面的考察,下面是本单元作业的具体总结。

一、JML语言的梳理和相关应用链情况

1.JML概述

使用 JML 来说明性地描述所希望的类和方法的行为,可以显著地改善整个开发过程。将建模表示法添加到 Java 代码中,其好处包括以下几点:

能更加精确地描述代码所完成的任务

能有效地发现和纠正错误

能减少随着应用程序的进展而引入错误的机会

能较早地发现客户没有正确使用类

能产生始终与应用程序代码保持同步的精确文档

JML 注释始终位于 Java 注解(comment)内部,因此它们不会对进行正常编译的代码产生影响。当我们想将类的实际行为与其 JML 规范进行比较时,可以使用开放源码 JML 编译器。用 JML 编译器编译过的代码如果没有做到规范中规定它应该做的事,那么该代码在运行时会抛出 JML 异常。这不仅能捕获代码中的错误,还能确保文档(JML 注释格式)与代码保持同步。

2.JML语法简介

  • 原子表达式
    \result:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。

    \old(expr):表示一个表达式在相应方法执行前的取值,该表达式涉及到评估中的对象是否发生变化。
    如果是引用(如HashMap),对象没改变,但进行了插入或删除操作。v和odd(v)也有相同的取值。

    \not_assigned(x,y,...):用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。

    \not_modified(x,y,...):该表达式限制括号中的变量在方法执行期间的取值未发生变化。

    \nonnullelements(container):表示container对象中存储的对象不会有null。

    \type(type):返回类型type对应的类型(Class),如type(boolean)为Boolean.TYPE。TYPE是JML采用的缩略表示,等同于Java中的 java.lang.Class。

    \typeof(expr):该表达式返回expr对应的准确类型。如\typeof(false)为Boolean.TYPE。

  • 量化表达式
    \forall:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
    \exists:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
    \sum:返回给定范围内的表达式的和。

  • 方法规格
    定义前置条件和满足后置条件的东西。

    前置条件:对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。requires P;其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。多个分开的requires是并列关系都要满足,或关系用requires P1||P2;

    后置条件:对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。ensures P;

    副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。JML提供了副作用约束子句,使用关键词assignable(表示可赋值)或者modifiable(可修改)。

    signals (Exception e) b_expr;
    signals_only (Exception e);
    强调满足前置条件抛出相应异常。

  • 类型规格

    不变式invariant

    状态变化约束constraint

3.应用工具链

  • 使用OpenJML检查JML规格的正确性,包括JML语法静态检查,代码静态检查,运行时检查。
  • 使用JML UnitNG根据JML语言自动生成测试检查代码正确性。
  • 使用SMT Solver验证代码等价性。

二、部署SMT Solver

在官网http://www.openjml.org/下载完openjml包后即可使用,本人在运行openjml时检测Group.java总是出现莫名的报错,如下图所示:

由此我自己写了一个简单的代码如下:

public class Main {
    /*@
      @ requires args != null
      @*/
    public static void main(String[] args) {
        System.out.println(args);
    }
}

运行后正常报错如下:

在修改后即可通过检查。

三、部署JMLUnitNG

在运行jmlunitng时也出现了莫名报错的情况,让我很是无奈,这次运行Group.java和自己写的代码都跑不通,无奈只好放弃,期待在研讨课上能够听取一些跑通了的大佬的教学。

四、作业架构梳理

(1)由于三次作业是层层迭代的,这里只放置关于第三次作业的UML图

(2)关于模型构建策略

我认为这三次作业难度最大的有两个地方,一个是算法性能上的要求比较苛刻,另一个是一些方法在实现上的困难。下面对三次作业中的一些重要方法进行进行分析:

①isCircle是寻找两点之间的通路问题,在图论中我们已经学习了用BFS或DFS深度搜索来寻找通路,在前两次作业中本人也运用了DFS的方法来解决这个问题。在第三次作业中由于新增了queryBlockSum方法,该方法主要使用了并查集的思想,并查集同样也能很好地解决isCircle的问题,因此第三次作业中就放弃了BFS的使用。

②queryMinPath是寻找两点之间的最短通路问题,该问题目前主要有Dijkstra和Floyd算法,二者的区别在于Dijkstra用于单源点到其他各点的最短通路,而Floyd主要用于多源点到其他个点的最短通路,考虑到算法的复杂度和实用性,我最终选择了Dijkstra算法。

③本人认为queryStrongLinked方法是本单元作业中最难实现的方法,首先我们需要补充关于点双联通分量的相关知识,发现该方法其实就是求证两点是否处于同一个双联通分量的问题。关于求双联通分量,有一个著名的Tarjan算法,该算法基于BFS和堆栈实现,但实现起来较为困难,本人在实践过程中总出现莫名的bug,最后放弃了该方法。本人认为一个简单的方法就是讨论区里某大佬提出的遍历路径上所有点的方法,该方法主要是先通过BFS/DFS找到两点之间的一条通路,然后遍历路径上出两端点外的所有点,求去掉该点后两端点之间是否有其他通路,如果没有则不属于同一个双联通分量,如果去除每一个点后都有,则属于一个双联通分量。该方法主要基于割点同双联通分量之间的关系,简单易懂,实现起来没有那么复杂。

④关于queryBlockSum主要是求people中含有几个不相连的子图的问题,该问题可以通过并查集完美解决,本质上就是返回并查集有几个集合,同时并查集也可以解决isCircle问题,是一个实用的算法。

本单元作业挑战最大的就是复杂度问题,O(n2)的复杂度评测时容易直接暴毙,因此许多需要双重遍历的方法我都通过增加NetWork属性的方法提前存起来,以便用时直接返回或只需遍历一遍返回。

(3)第三次作业的复杂度分析

MyGroup类:

MyPerson类:

MyNetWork类:

五、作业bug分析

(1)第一次作业没有bug

(2)第二次作业由于第一次作业的侥幸心理,导致没有充分测试,出现了两个致命的bug。一个是在求平均值的时候没有考虑到分母是0的问题,导致强侧直接暴毙。一个是在addGroup的时候判断条件写错,同样导致强侧直接暴毙。因此总结第二次作业的bug主要原因是提交前没有充分测试,一些通过测试很容易找出来的bug没有发现。

(3)吸取第二次作业的教训,我在第三次作业上做了充分地测试,终于在强测试没有出现致命的暴毙,但却有4个测试点倒在了CTLE上,千测万测没有测到复杂度上。主要原因是由于许多方法针对复杂度问题都进行了提前优化,导致addRelation的复杂度较高,另外是queryMinPath利用Dijkstra算法时没有在找到id2时立即退出,而是等待全部点都被找到才推出,提升了算法的运行时间。

六、阐述对规格撰写和理解上的心得体会

相对于前两个单元的作业,本单元的作业确实压力小很多,具体的算法实现并不难,重点在于怎样才能不TLE

理解JML规格设计,理解写好的JML注释,使自己的代码符合规范,保证正确性。

JML规格可以避免自然语言的二义性,自然语言的解释歧义,表述模糊完全可以通过数学语言解释清楚,保证了编写代码的严谨性和正确性。前两次作业是完全按照所给的规格规范实现的,可以很好的帮助整理思路。而第三次作业给的JML规格注释并不像前两次一样落在实处,这也正反应出JML规格对于功能复杂的函数,想要完全依靠数学语言表述清楚还是比较繁琐的。

随着OO课程的不断学习,我越发感觉到,代码实现其实是最后的,占时间最少的,真正需要花费时间的是思考作业的架构设计,实现策略,这也正是JML规格设计所需要的。

最后,Junit测试也是很有效的一种测试方法,构造测试样例能对自己的方法进行覆盖性的测试。

posted @ 2020-05-23 12:29  qsf  阅读(151)  评论(0编辑  收藏  举报