BUAA_OO_第三单元作业总结

BUAA_OO_第三单元作业总结

一、JML语言相关

1.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等工具
以静态方式来检查代码实现对规格的满足情况。

2.JML理论基础

注释结构

JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式
//@annotation ,块注释的方式为 /* @ annotation @*/ 。需要注意的是,规格中的每个子句都必须以分号结尾,否则会导致JML工具无法解析。

JML表达式与操作符

  • \result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
  • \old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
  • \not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
  • \nonnullelements( container )表达式:表示 container 对象中存储的对象不会有 null 。
  • \not_modified(x,y,...)表达式:限制括号中的变量在方法执行期间的取值未发生变化。
  • \forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
  • \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
  • \sum表达式:返回给定范围内的表达式的和。
  • \product表达式:返回给定范围内的表达式的连乘结果。
  • \max表达式:返回给定范围内的表达式的最大值。
  • \min表达式:返回给定范围内的表达式的最小值
  • \num_of表达式:返回指定变量中满足相应条件的取值个数。
  • 子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则假。
  • 等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2
  • 推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1

方法规格

  • requires:前置条件通过requires子句来表示
  • ensures:后置条件通过ensures子句来表示
  • assignable/modifiable:副作用范围限定(side-effects)。其中用到了两个关键词\nothing 和 \everything ,前者表示当前作用域内可见的所有类成员变量和方法输入对象都不可以赋值或者修改。后者表示当前作用域内可见的所有类成员变量和方法输入对象都可以赋值或者修改。
  • signals:结构为 signals (Exception e) b_expr。意思是当 b_expr 为 true 时抛出异常e。与之类似的的是signals_only,只要满足前置条件时就抛出相应的异常。

类型规格

  • 不变式invariant:在所有可见状态下都必须满足的特性。
  • 状态变化约束constraint:对前序可见状态和当前可见状态的关系进行约束。
  • 继承相关:子类必须满足父类的规格约束,在此原则上,也可增添新的约束。

3. JML工具链

这一单元中,我们用到的JML相关工具有Openjml、JMLUnitNG、JUnit。其中Openjml用于检查给定的jml语言是否符合语法,以及检查java程序是否满足给定的jml规格。JMLUnitNG与JUnit则是根据给定的jml自动生成测试样例。

二、SMT Solver

三、JMLUnitNG/JMLUnit的使用

编写测试类:

package test;


import org.junit.Test;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.junit.Assert;


public class MyPathTest {
    private MyPath p1, p2;
    @Test
    public void testIsValid() {
        Assert.assertTrue(!p1.isValid());
        Assert.assertTrue(p2.isValid());
        p2.isValid();
        p1.isValid();
    }

    @BeforeMethod
    public void setUp() {
        int[] a = {1,2};
        int[] b = {1};
        p1 = new MyPath(b);
        p2 = new MyPath(a);
    }

    @AfterMethod
    public void tearDown() {
    }


}

结果:

四、架构设计

第一次

这一次作业的设计比较简单,基本上按照规格给出实现即可。

第二次

这一次求最短路由于边权都为1,所以我采用了bfs算法,采用了两层hashmap实现了一个类似于邻接表的数据结构。个人在做的时候就感受到了使用hashmap的不便,不如矩阵数据那样可以直接用dis[i][j]取值或赋值。可以改进的地方是增加一层节点类,这样就能使操作更加简便,思路更清晰。

第三次

类图

复杂度

这一次的作业个人认为设计比较失败,从类图中也可以看到设计的臃肿。原因在于自己走了很多弯路。开始时,我封装了一层节点类,保存了节点id,该节点邻接的所有节点和所有包含该节点id的路径id。由此,我想利用dfs求出两节点之间的所有路径,然后根据需求分别找出最佳的路径。但当自己实现了这一想法时,才发现连随机生成的100个节点组成的路径都会超时。经过反省,我发现求出所有路径的复杂度最高可以达到O(2^n),这显然是不适合的做法。类图中vertex这个类就是失败做法的产物。同时,为了尽量减少对上一次作业的修改,我将上一次的数据结构大部分都保存了下来,实际上是做了重复工作,让代码更加臃肿。

在看了讨论区zyy和xwl大佬的做法后,我学习了拆点的做法,即对于每个地铁站node,拆分成2+x个点,其中x为经过这个地铁站的Path数。前两个点为抽象出来的起点和终点,再封装一个Node类,结构为(nodeId, pathId),设置起点为 (nodeId, -1),终点为 (nodeId, 0)每次查询只需要查询 (nodeId, -1)到 (nodeId, 0)的最短路径即可。这样就建好了图,对于最小票价、换乘、不满意度三类需求,赋予不同的边权,再实现dijkstra算法即可。

这次作业的实现还有一点缺陷,即三种计算方法均写成了单独的函数,然而其中有大量的重复,导致一处修改就要改三处地方,十分不便。没有用任何的设计模式。好的办法应该利用工厂模式建立三种不同的图,而使用统一的计算模块。

五、BUG及修复

这一单元个人写出的bug不少,而且几乎都是致命的正确性错误。经过反思,个人发现许多bug都十分不应该出现。

第一次作业中,我是理解错了字典序的含义,认为是将单独将两条路径的每一个对应节点进行字典序比较,即将每一个int类型的节点转换成string进行比较,而非真正的字典序比较。同时,由于主观上的懈怠和忽视,没有进行自己测试。这导致了我的强测几乎全炸。

第二次作业中,吸取了上一次作业的教训,我在实现要求之后,写了随机数据生成器,测试了部分超过互测数据限制的数据,经过和同学的对拍,没有发现问题,最后互测也果然没有任何问题。

第三次作业里,由于前一章节所述,自己在设计时没有考虑设计的合理性,没有进行复杂度计算,导致基本快写完时才发现自己的程序几乎不可使用,只能重构。这导致了这次作业时间十分紧迫,几乎是压线提交,只测试了自己手写的几条数据,没有进行对拍。这导致了自己的程序出现了正确性和超时问题。正确性问题是在add、remove时应设置信号,使得三类图计算需求时应清除原来的缓存,在新的图结构重新计算。然而我三类计算利用了同一信号量,导致了互相之间受干扰,一个图计算完成后将信号量清零,导致了其余两类计算没有准确的清零,产生了正确性错误。修复措施是将信号量一分为三,各自独立置位清零。超时问题出现在Dijkstra计算中。在Dijkstra算法里,设S为已算出最短路径的点集。每次选中一点加入S后,要修改相应的路径。个人犯的错误是每次修改权值时都遍历所有与S中顶点相邻的点。事实上只需要遍历与新加入的点相邻的顶点即可。之前的做法做了很多无用功,导致超时。

六、心得与体会

在规格的撰写方面,个人认为jml相关的语法已经做出了清楚的规定,只要符合语法要求,写出需求对应的规格不是很难,还可以利用Openjml进行语法验证。而根据规格给出实现则是对自己的代码水平,尤其是数据结构水平的考验。规格只严格给出了“做什么”的要求,而“怎样做”就是对我们的考验。这次作业里,个人的问题基本上都出在数据结构上,包括字典序的含义、dijsktra的实现细节等等。因此,这一单元是对个人素质的综合考验,既要细心留意规格中不同逻辑的区别,又要给出正确的具体实现。

posted @ 2019-05-22 21:57  fubuki  阅读(193)  评论(0编辑  收藏  举报