你以为我是多线程,其实我是JML哒

上次电梯作业总结中,我如是说道:

虽然不知道下次作业是啥,但肯定和多线程脱不开干系

img

果然,我还是太年轻了,在多线程正如火如荼的时候立刻进入了新的篇章,而且三次作业中竟然没有和多线程扯上一丝一毫的关系。

说实话,这一单元应该算得上是比较亲民了,至少前两次作业的确比电梯和多项式轻松了一些。

对于第三次作业,其实感觉花在优化算法上面的时间过多了(主要是心虚~~~,不知道在评测机上会慢多长时间),导致有一些优化过头了,在细枝末节上花费了较多的时间。

闲话少叙,让我们开门见山地进入这次的主题吧。


JML应用工具连

在官网中,有如下工具链,其中有着我们耳熟能详的junit

  • jml-launcher (the launcher for the graphical user interface tools).
  • jml and jml-gui (the checker).
  • jmlc and jmlc-gui (compile for runtime assertion checking).
  • jmldoc and jmldoc-gui (a version of javadoc that includes JML specification information).
  • jmle (compile for executing or prototyping specifications).
  • jmlrac (a version of java, the VM, that includes the bin/jmlruntime.jar file in the CLASSPATH, for running files compiled with jmlc).
  • jmlre (a version of java, the VM, that includes the runtime support needed for executing specifications, for running files compiled with jmle).
  • jmlspec and jmlspec-gui (generate skeleton specification files from Java source files).
  • jmlunit and jmlunit-gui (produce unit testing code stubs for use with JUnit).
  • jtest (combines the work of jmlc and jmlunit)
  • jml-junit (a version of JUnit's swing user interface that includes the bin/jmlruntime.jar and bin/jmljunitruntime.jar files in the CLASSPATH, for running JML and JUnit based tests on files compiled with jmlc and test cases generated by jmlunit).

JML理论基础

纵观JML整体,其实较为简单明确,也易于理解,就谈一下我个人的理解吧。

对于类规格来说,不变式和状态变化约束是其核心部分。

而作为方法规格,根基的就是以下三者:

  • 前置条件(requires)
  • 副作用(assignable)
  • 后置条件(ensures)

当然,这三者也是实现一个方法所要关注的核心问题。

而其他语法,例如各类表达式,是将类规格和方法规格具象化的手段。

如果将JML比作一栋大厦,那么类规格和方法规模就是大厦的根基,而其他语法是砖瓦,二者相辅相成,大厦才能牢固。

JUNITNG体验,一波三折的过程

最开始,我真的不知道有JUNITNG这个东西,最近两天尝试了一下,其实感觉并不怎么好用(也有可能是我太菜了,并不太会用……)

第一折

最开始试用的时候,那可真的是满屏报错,就像这样

第二折

其实后来发现是JUNITNG支持不了较为复杂的JML语法,因此将其改的简单一些,并按如下脚本运行

$ bash testng demo/Demo.java
$ javac -cp testng.jar demo/*.java
$ javac -cp testng.jar demo/Demo_JML_Data/*.java
$ bash openjml -rac demo/Demo.java
$ java -cp testng.jar demo.Demo_JML_Test

终于得到了正确的测试结果

经过对多个方法进行测试,发现TestNG只能生成边界数据且生成数据唯一,很谜

第三折

在得到了正确结果之后,我突发奇想,加了如下规格:

/*@ requires a<100;

我想,这下你总不会给我生成MAXINTEGER了吧,但事实上,我错了,TestNG依然我行我素,依然生成了2147483647,唉,孺子不可教也。

img

我枯了,果然童话里都是骗人的。

JUNIT的使用

在这几次作业中,我也或多或少地使用了JUNIT,例如检查getLeastTicketPrice方法

    public void testGetLeastTicketPrice() throws Exception {
		//TODO: Test goes here...
        Assert.assertEquals(rs.getLeastTicketPrice(1,2),1);
        Assert.assertEquals(rs.getLeastTicketPrice(2,7),4);
        rs.addPath(p3);
        Assert.assertEquals(rs.getLeastTicketPrice(2,7),4);
        Assert.assertEquals(rs.getLeastTicketPrice(3,3),0);
        rs.removePath(p3);
    }

但对于最后一次作业来说,其实并不适合采用这种方法进行测试,因为人脑也很难算出准确的结果值供单元测试使用。

因此,我最后还是回归到了与同学对拍检测正确性的道路上。

代码构架

由于这几次代码重用部分较大,因此只使用第三次作业的代码进行分析。

在第三次作业中,我采用了拆点Dijkstra的方法,并为每一个点加入了根点供换乘时使用。

在GraphList中保存了三种计数方式对应的邻接表(用HashMap实现)供Dijkstra算法使用,并将Dijkstra算法独立出来作为一个新的类,在Dijkstra类中保存了最短路径的值(即缓存),地铁系统只能查询最短路径计算结果而不能更改最短路径的值。

Pair对象中保存了邻接点和权值信息,用来作为邻接表的value(其实真正的value是由Pair对象组成的ArrayList)。

这三次作业,几乎每一次我都会直接粘贴上一次的全部代码而非进行继承,说实话,这并不是一种好的行为,但继承后无法访问父类对象着实是一个令人头疼的地方,因此我想,是不是不应该绝对禁止protected对象的使用呢,如果可以使用protected关键字,进行继承的时候也就不会望private兴叹了。

其实也可以使用set/get方法来解决这个问题,但就我的水平而言,这样做的话代码反而显得有些臃肿,因为对于这次作业来说容器种类较多,增删改查也十分频繁。

其实归根结底,还是懒了~~~得想办法改掉这个毛病。

Class OCavg WMC
oo.Dijkstra 1.75 14.0
oo.GraphList 1.6 8.0
oo.Main 1.33 4.0
oo.MyPath 1.77 23.0
oo.MyRailwaySystem 3.22 87.0
oo.Pair 1.29 9.0

复杂度其实基本无所谓,也看不出什么来。

Bug

这三次作业中,我在强测和互测中没有被找到bug,而在第二次和第三次互测中发现了一些其他同学的bug。

复现方式如下:

PATH_ADD 0 0
PATH_REMOVE 0 0

当路径的后两个数字相同时,删除该路径就会抛出NullPointerException异常

原因应该是在删除最后一条边的时候直接删除了相连的两个点导致访问最后一个点的时候程序找不到对应的位置而导致出错。

别问我怎么知道的,问就是我也犯过这个错。

心得与体会

如果打一个不太恰当的比方,以前我们的OO程序像是拿着自己的乐器无拘无束地进行演奏(简称:为所欲为),而这一单元更像是乐队中来了一位指挥(简称:你给我收敛点),其实,我觉得这才应该是对于初学者最适合的方式,毕竟对于构架还一知半解的我们很难设计出可拓展性高、优雅美观的代码,保证了正确性就万事大吉了。

因此,是否应该考虑下一届将JML提前到第一单元或第二单元呢?

img

就剩最后一个单元了,加油吧。

愿指引明路的苍蓝星为我们闪烁