OO第三单元总结——小满

目录
  • 一、JML的梳理与总结
    • JML理论基础
    • 应用工具链
  • 二、SMT Solver的部署与验证
    • 部署
    • 形式验证
  • 三、JMLUnitNG的部署与测试
    • 部署
    • 测试
  • 四、梳理自己的架构设计
    • 第一次作业
      • 架构
      • 分析模型构建策略
    • 第二次作业
      • 架构
      • 分析模型构建策略
    • 第三次作业
      • 架构
      • 分析模型构建策略
  • 五、分析自己程序的bug
    • 分析未通过的公测用例和被互测发现的bug
    • bug修复情况
  • 六、心得体会
    • 规格撰写
    • 规格理解

一、JML的梳理与总结

JML理论基础

由于很多jml博客 对理论基础总结的很好,而且指导书也会讲解基础语法,我就不再赘述。

我这里就分享一下自己在初学jml语法时,对jml语法总结的难点,供初学者参考:

(1) \old

  • \old(expr)表达式:用来表示一个表达式expr在相应方法执行前的取值。该表达式涉及到评估expr中的对象是否发生变化,遵从Java的引用规则,即针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。

(2)\sum

  • 形如(\sum T x; P(x) ; k) 意思是满足P条件的x有一个表达式就加上k

(3)推理操作符

  • 只要不是结果false条件true就成立

  • b_expr1==>b_expr2或者b_expr2<==b_expr1。对于表达式b_expr1==>b_expr2而言,当b_expr1==false,或者b_expr1==trueb_expr2==true时,整个表达式的值为true

(4)断言

  • 特别需要提醒,在JML断言中,不可以使用带有赋值语义的操作符,如++,--,+=等操作符,因为这样的操作符会对被限制的相关变量的状态进行修改,产生副作用。

(5)多个requires子句是并列,都得满足

  • 注意,方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。如果设计者想要表达或的逻辑,则应该使用一个requires子句,在其中的谓词P中使用逻辑或操作符来表示相应的约束场景:requires P1||P2;

(6)不变式invariant

  • 在方法执行期间,对象的不变式有可能不满足(是不可见状态)。因此,类型规格强调在任意可见状态下都要满足不变式。

应用工具链

可以应用的主要jml工具是OpenJMLJMLUnitNG:

  • OpenJML:首选的JML相关工具,以提供全面且支持最新Java标准的JML相关支持为目标,能够进行静态规格检查(ESC,Extended Static Cheking)、运行时规格检查(RAC,Runtime Assertion Checking)和形式化验证等一系列功能。OpenJML提供了自带的命令行版本和Eclipse插件版本。

  • JMLUnitNG:JMLUnit的替代工具,能够根据JML规格自动生成基于测试库TestNG的单元测试集。

我看了前人的博客 ,得知IDEA对JML插件的支持并不好,就下载了一个Eclipse专门来配置插件。

配置的方法参考了这篇配置Eclipse插件的博客 ,在此表示感谢。我看的博客的电脑是windows操作系统,我的电脑是deepin操作系统,本以为因为操作系统不同会出一些问题。不过,下载插件只要在Eclipse里面输入下载地址就可以下载好,甚至不需要选择不同操作系统的文件下载类型,一键安装就完成了,非常方便(如下图)。

OO第三单元总结——小满_第1张图片

最后下载好发现有很多功能(如下图),我目前只会用其中的静态规格检查(ESC)和形式化验证功能,更多的功能有待掌握。

OO第三单元总结——小满_第2张图片

二、SMT Solver的部署与验证

部署

Eclipse安装好OpenJML插件之后,就其实已经帮我们下载好SMT Solver了。

我在Eclipseplugins文件夹里面发现了一个叫org.jmlspecs.Solvers_1.6.0的文件夹(如下图),打开一看里面就有需要用的Solver——z3

OO第三单元总结——小满_第3张图片

在Eclipse里面Window -> Preferences -> OpenJML -> OpenJML Solvers->设置配置,把路径定义为z3的路径。

形式验证

在Eclipse的JML菜单里选择静态规格检查(ESC)功能,可以对目标的java文件进行形式验证。

我对第一次作业的MyNetwork.java进行了形式验证,发现addPerson这个方法是不符合形式验证的。

OO第三单元总结——小满_第4张图片

黄色高亮信息显示的是:

Method addPerson overrides parent class methods and so its specification should begin with 'also'

[INVALID]信息显示的是:

OO第三单元总结——小满_第5张图片

注意到信息里面的assertion出错的是PossiblyNullDeReference,应该是有null指针的问题。

三、JMLUnitNG的部署与测试

部署

本人的部署操作是借鉴这篇博客 的,在此表示万分感谢,通过这个方法无报错地实现了JMLUnitNG测试。

1. 在官网上OpenJML的压缩包,下载JMLUnitNG的jar包,放到OpenJML的文件夹里。
2. 运行`java -jar openjml.jar "$@"`可以看到能用的对应jar包功能
3. 新建一个文件夹test,里面放入需要自动生成测试的java文件
4. 运行`java -jar jmlunitng.jar demo/*.java`会在test文件夹生成一系列测试文件
5. 编译所有文件执行命令`java -jar openjml.jar -rac  `,得到rac文件
6. 执行命令`java -cp jmlunitng.jar test/Demo_JML_Test`,就可以看到测试结果

测试

测试结果如下:

OO第三单元总结——小满_第6张图片

用的测试类是Group接口里2个比较简单的方法构成的,所以自动生成的8个测试点都通过了。

发现测试用例生成的都是边界上的数据,比如针对整型变量的0、2147483647、-2147483647。

这种边界数据的测试可以帮助我们发现自己的错误,比如溢出、除0之类的错误。

四、梳理自己的架构设计

第一次作业

架构

OO第三单元总结——小满_第7张图片

分析模型构建策略

由于第一次作业比较简单,所以我只是在容器的选择上有自己的设计。

存储人的容器不是用jml规格写的数组,而是用hashmap来存储从人的id到认识的人的映射。这样查找起来可以利用hash值查找的优势,避免遍历数组查找。

第二次作业

架构

OO第三单元总结——小满_第8张图片

分析模型构建策略

第二次作业增加了很多指令,而且规定了时间约束,所以要开始考虑算法的复杂度问题了。

我采用了缓存的策略

对于group类的查询方法,可以大致分为2类:

  • 直接返回一个sum值,比如getValueSum
  • 不是直接返回一个sum值,比如getAgeVar

但是这2者都是要用到缓存的信息的,所以缓存的时候,我缓存了

private int oldPersonListLen;
private int relationSum;
private int valueSum;
private BigInteger conflictSum;
private int ageSum;

这5个信息,是足够满足题目要求的。

所以,每当来一个group类的查询请求,我先检查是不是更新(比对oldPersonListLen与现在的personList.size()是否相等)

  • 如果更新了,就把缓存的5个值更新一波。再返回查询值。
  • 如果没更新,直接返回查询值。

返回查询值的时候

  • 如果是类似getValueSum的,就可以直接返回。
  • 如果是不能直接返回的,类似getAgeVar,就当场根据公式算一遍返回值。

这样的策略只是为了省一些计算时间,防止ctle

第三次作业

架构

OO第三单元总结——小满_第9张图片

分析模型构建策略

第三次作业的难度,比前2次增加了很多,有一些复杂的图论算法需要我们去掌握,我尽量采用的是简单而不易出错的策略。

(1) isCircle

前2次作业的isCircle都采用的是bfs去找是否联通,但是这次作业由于有其他方法要调用多次isCircle,就不得不考虑更快的方法了。

我使用了群友推荐的并查集的方法,单独写了一个并查集的类,衔接到原来的架构里。

并查集搜联通果然很好,速度又快,又好写。

(2) queryBlockSum

这个查询联通分量的方法也可以用并查集做,只要遍历所有的人,看有多少种不同的树根,速度也很快。

(3) queryStrongLinked

我把原来的 isCirclebfs版本留下了,改成返回一条联通的边

这样就可以用到queryStrongLinked里面

所以就先考虑id1id2直接相连的情况。再判断id1id2是否连通,再枚举去掉这条边上其他n-2个点,每次去掉一个点,判断id1id2是否连通。如果枚举完了,都是连通,说明id1id2满足。

(4) queryMinPath

单独写了一个Dijkstra类来实现优先队列Dijkstra算法,并且用了一个新的Edge类来记录边的信息,计算最短路径。

群里还有人说可以不把起点到所有点的最短路径算完,只要算到目的地就可以了。但是我还是没有改,算完所有点,最后时间还是够的。

五、分析自己程序的bug

分析未通过的公测用例和被互测发现的bug

(1)强测

三次强测都没有发现bug。

(2)互测

在第二次互测中被hack了9次,发现是其实是一个"莫须有"的bug。

因为从第一次作业过渡到第二次作业的时候,我把每个方法的jml规格都重新看了一遍,就害怕有改变。

果然发现了第一次作业中Network的addRelation方法有如下的JML

 @ also
 @ public normal_behavior
 @ requires id1 == id2 && contains(id1);
 @ assignable \nothing;

但是在第二次作业里这个被删掉了。然后,讨论区里的助教就说"如果规格中没有满足目前条件的分支,则此方法不能执行任何操作,应直接return"

我把原来的 id1 == id2 && !contains(id1)就报错的语句给删掉了,改成直接return。

果然,强测遵守jml约定,没有出现 id1 == id2

但是,互测时,这个约定就貌似被忽略了

于是,大伙都可以提交addrelation id1 == id2 && !contains(id1)的指令来hack我QAQ

bug修复情况

第二次互测,由于规格约束的冲突,我就只能又改回去第一次作业的规格要求的版本提交bug修复了。

六、心得体会

规格撰写

  • 撰写规格的时候,由于只能用数组表示数据集合,没有容器,所以要从跟抽象的角度思考描述。
  • 可以通过调用已有的方法来撰写规格,避免重复描述。

规格理解

  • 在理解规格的时候,切忌照抄规格,要根据规格用合适的数据类型和实现方式,写出自己的代码。
  • 可以通过JUnit测试来检查自己对规格的理解是否准确。
  • 也可以进行形式验证来检查自己对规格的理解是否准确。

你可能感兴趣的:(OO第三单元总结——小满)