面向对象第三次作业总结

前言:

这是一篇面向对象作业总结,内容是根据jml规格编写一个地铁路径查询系统。

JML语言及工具

       Jml语言是java程序的统一建模语言,它描述了一种通用的规格语法,使得设计人员可以用严谨的语言描述其设计思路。在编程前设计出程序的jml,则可以在规格的指导下进行开发,这是一种比较理想的状态。然而不是经验丰富的架构师,很难马上想出程序的架构,往往是借鉴一些代码然后边写边看,在最终完成后把自己所想用规格整理出来,使得别人对其接口有更加严谨的理解。

       Jml语言主要处理方法的规格和类的规格。

       针对方法规格,应用等价类划分的方法,把输入空间划分成不同的子空间,这在jml的体现是requires语句,对于每个public normal_behavior和public exceptional_behavior,都应该有一个requires描述子空间。

       每个方法还需要有后置条件,就是描述最终结果的语句。这一点在jml里以ensures实现,除此之外还有signals用以描述将会抛出的异常。如果对于方法内部隐含的改变,则需要用assignable语句加以说明。

       在类中,首先就是要有数据,对象一开始都是数据,后来才有了面向对象,让对象包括其方法。Jml不能描述一个真实的数据,因为规格是数学意义上的,只需要描述输入输出,而不是严格描述算法实现。所以用public instance model描述数据。

       除此之外,还有不变式的要求,就是每个方法调用前后都要保证数据满足某个属性,jml里使用了invariant实现不变式的描述。而constraint语句则是用来抽取所有后置条件共性的语句,也就是如果要改变类,那么改变必须都满足constraint所描述的内容。

       在描述表达式的过程中,全称量词用forall表示,存在量词用foreach表示,或与非也是表达式中的一部分,用 esult表示返回值,用old量词表示调用前的值,这些都符合离散数学的要求。

Jml依赖工具:

       Openjml是一个jml的具体实现,它很好地支持了eclipse,利用jml对程序进行一些辅助开发。

       Openjml主要有三个工具,typecheck是用来检查jml语法是否正确的,是一个比较快速的方法。

而esc则是用以进行代码静态检查的,其原理就是将jml变成SMT格式的真值表达式,然后对每个方法,用一些例子验证方法的输入输出是否满足条件。为了用SMT进行真值比较,必须部署z3或者别的SMT Solver,求解真值表达式。

Rac则是进行动态代码检查的,他会生成与javac编译得到.class不同的.class文件,插入一些assert断言,当运行main方法时,就会边执行程序,边进行断言判断。

设计架构分析

第一次作业类图如下:

第一次作业的架构比较简单,完全就是实现接口而已。然而这个架构没有考虑到性能问题,所以导致每次数节点时,都要重新构建集合,故而速度较慢。

第二次作业类图如下:

       针对第一次作业出现的问题,第二次作业在path中增加的改进是:仅在需要时暂存path的DistinctNodeCount所需的Set,而不是每次创建path就创建Set。

       用nodeSet保持每个节点在整个图中出现的次数,当使用remove的时候次数为0时才会移除节点,而不是每次remove都重建图。

       MyGraph和MyGraphFld其实可以看成都是继承于MyPathContainer的类,然而我为了方便访问原来MyPathContainer类里的数据,就直接把代码粘贴过来了。

MyGraphFld新加了一个innerGrp,需要在addPath的同时构建一个连接边表,然后通过floyd算法,每构建一张图时更新一次全图最短路,时间复杂度O(n^3),在测试中肯定会超时的。

MyGraph时最终使用的类,基本上就是MyPathContainer,只不过多了dpMap,这个数据是对bfs算法的封装,用addConnection和rmvConnection对图进行更新。内部依旧是保持一个连接边表,还有就是暂存了最短路信息

作业三的类图如下:

       中心的MySubWay和左下方的HelperMap相当于原来的MyGraph和dpMap,除了HelperMap中使用深度优先搜索统计连通分量外,没有别的改变。

       HelperSubway则是封装了后来需要的最短路算法,也就是用dijkstra。Dijkstra算法框架是统一于getDijkAns方法里的,而这个方法的主要参数就是起止节点、暂存容器和函数指针。暂存容器采用的都是第一在HelperSubway里的数据,函数指针则是采用了HelperSubwayOrgInterface接口。实现这个接口的类有三个,这三个都提供了两点之间边权值计算的方法,和换乘代价计算的方法。ComHelper是一个抽象类,本质上就是接受两个节点,然后遍历所有可能的节点所属路径的组合,算出最小代价,并再一次缓存。这个过程也是有共性的代码,所不同的只是调用那个最短路算法而已。它与MySubway共享HelperSubway。它的三个实现就是重载了不同的renew方法和sepcFunc方法。

       然而,对于最少换乘则另外使用了HelperTrans类,而不是用dijkstra。因为最小换乘可以将路径缩点,然后用广度优先算最短路,算法快了很多。

       如果直接遍历所有拆点组合,将会弄出很多多余的dijkstra算法,减少的方法如下。可以每算出一个dijkstra,就构造一个可确定点集,意思是这里面的点到起始节点的路径里都会在最开始出现一个换乘,免去这个换乘则得到的一定是最短路。这个点集里一开始只有初始点的换乘点。遍历其他点,得到最短路,如果最短路经过这个点集,就代表这条路上所有点都可确定,这样就扩大了集合。每确定一个点,那么头结点的换乘点到此点的最短路都可以设置为这个最短路,这是因为我们并不关系头结点的换乘。这样就可以减少dijkstra计算次数。

Bug分析

       第一个bug就是超时,这个要从设计上进行解决,具体方法就是应用缓存机制,并且尽量保持所有中间结果。还有一点就是要用优先队列。

       第二点就是要简单建图,其实建图的过程并不会占用太多cpu,完全可以每次清空邻接表,重新建图。如果数有多少个重边然后进行删除的话,容易出bug,我就是犯了这个错误。

       第三点,不能进行path内算一次最短路径,path间再用最短路算法,那是不正确的算法。

       测试的策略肯定时随机生成测试用例的,由于addPath和removePath都是占很少数的,所以要将其概率调大。节点当然是越满越好,图就越复杂,我根据数据保证的节点数(比如250)作为基,对节点编号取模,保证图规模受限。最短路的测量为主,为了测时间复杂度,可以把其他所有方法都用最短路取代。一般而言这样很容易测出超时。

部署JMLUnitNG并针对Graph接口的实现进行测试

首先将编码改为gbk并清理掉中文,而后下载jmlunitng-1_4.jar工具。

在windows上执行java -jar jmlunitng-1_4.jar -cp oo_hmk_3_3_gbksrc oo_hmk_3_3_gbksrcmycodeMyGraph.java指令,对MyGraph生成测试。

       我的工程都放在了oo_hmk_3_3_gbksrc里,且要求内部不含new HashMap<>()的形式,而是将<>中内容补全。这样就可以得到一个MyGraph_JML_Test类,其中有main方法。在eclipse中执行它,可以得到TestNG的测试结果如下:

[TestNG] Running:
  Command line suite
 
Failed: racEnabled()
Passed: constructor MyGraph()
Passed: <<mycode.MyGraph@64cee07>>.addPath(null)
Passed: <<mycode.MyGraph@27abe2cd>>.containsEdge(-2147483648, -2147483648)
Passed: <<mycode.MyGraph@6fdb1f78>>.containsEdge(0, -2147483648)
Passed: <<mycode.MyGraph@51016012>>.containsEdge(2147483647, -2147483648)
Passed: <<mycode.MyGraph@29444d75>>.containsEdge(-2147483648, 0)
Passed: <<mycode.MyGraph@2280cdac>>.containsEdge(0, 0)
Passed: <<mycode.MyGraph@1517365b>>.containsEdge(2147483647, 0)
Passed: <<mycode.MyGraph@4fccd51b>>.containsEdge(-2147483648, 2147483647)
Passed: <<mycode.MyGraph@44e81672>>.containsEdge(0, 2147483647)
Passed: <<mycode.MyGraph@60215eee>>.containsEdge(2147483647, 2147483647)
Passed: <<mycode.MyGraph@4ca8195f>>.containsNode(-2147483648)
Passed: <<mycode.MyGraph@65e579dc>>.containsNode(0)
Passed: <<mycode.MyGraph@b065c63>>.containsNode(2147483647)
Passed: <<mycode.MyGraph@768debd>>.containsPathId(-2147483648)
Passed: <<mycode.MyGraph@490d6c15>>.containsPathId(0)
Passed: <<mycode.MyGraph@7d4793a8>>.containsPathId(2147483647)
Passed: <<mycode.MyGraph@449b2d27>>.containsPath(null)
Passed: <<mycode.MyGraph@5479e3f>>.getDistinctNodeCount()
Failed: <<mycode.MyGraph@4f4a7090>>.getPathById(-2147483648)
Failed: <<mycode.MyGraph@18ef96>>.getPathById(0)
Failed: <<mycode.MyGraph@6956de9>>.getPathById(2147483647)
Failed: <<mycode.MyGraph@769c9116>>.getPathId(null)
Failed: <<mycode.MyGraph@6aceb1a5>>.getShortestPathLength(-2147483648, -2147483648)
Failed: <<mycode.MyGraph@2d6d8735>>.getShortestPathLength(0, -2147483648)
Failed: <<mycode.MyGraph@ba4d54>>.getShortestPathLength(2147483647, -2147483648)
Failed: <<mycode.MyGraph@12bc6874>>.getShortestPathLength(-2147483648, 0)
Failed: <<mycode.MyGraph@de0a01f>>.getShortestPathLength(0, 0)
Failed: <<mycode.MyGraph@4c75cab9>>.getShortestPathLength(2147483647, 0)
Failed: <<mycode.MyGraph@1ef7fe8e>>.getShortestPathLength(-2147483648, 2147483647)
Failed: <<mycode.MyGraph@6f79caec>>.getShortestPathLength(0, 2147483647)
Failed: <<mycode.MyGraph@67117f44>>.getShortestPathLength(2147483647, 2147483647)
Failed: <<mycode.MyGraph@5d3411d>>.isConnected(-2147483648, -2147483648)
Failed: <<mycode.MyGraph@2471cca7>>.isConnected(0, -2147483648)
Failed: <<mycode.MyGraph@5fe5c6f>>.isConnected(2147483647, -2147483648)
Failed: <<mycode.MyGraph@6979e8cb>>.isConnected(-2147483648, 0)
Failed: <<mycode.MyGraph@763d9750>>.isConnected(0, 0)
Failed: <<mycode.MyGraph@5c0369c4>>.isConnected(2147483647, 0)
Failed: <<mycode.MyGraph@2be94b0f>>.isConnected(-2147483648, 2147483647)
Failed: <<mycode.MyGraph@d70c109>>.isConnected(0, 2147483647)
Failed: <<mycode.MyGraph@17ed40e0>>.isConnected(2147483647, 2147483647)
Failed: <<mycode.MyGraph@50675690>>.removePathById(-2147483648)
Failed: <<mycode.MyGraph@31b7dea0>>.removePathById(0)
Failed: <<mycode.MyGraph@3ac42916>>.removePathById(2147483647)
Failed: <<mycode.MyGraph@47d384ee>>.removePath(null)
Passed: <<mycode.MyGraph@2d6a9952>>.size()
 
===============================================
Command line suite
Total tests run: 47, Failures: 27, Skips: 0
===============================================

可以看出,生成测试用例的一般方法就是,对于int,生成0,-2147483648,2147483647,对于Object的子类,都用null进行测试。这种自动生成的测试一般都是很不完整的,因为没有构造出一个合适的上下文环境。

而且,这种测试没有jml也能生成,并没想象中的那么有效。

部署SMT Solver进行静态验证

改要求前测了一个方法,后续内容比较难所以没测了。

在MyPath下有方法:

    public int hashCode() {
        int sz = mynodes.size();
        int first = mynodes.get(0);
        int last = mynodes.get(sz - 1);
        return sz * 20000 + (first * first + last * last) % 1973;
}

其检测结果为

                     VALUE: sz      === 107374
                     VALUE: 20000        === 20000
                     VALUE: sz * 20000 === 2147480000
                     VALUE: first    === ( - 2147483392 )
                     VALUE: first    === ( - 2147483392 )
                     VALUE: first * first  === 65536
                     VALUE: last    === 2147482880
                     VALUE: last    === 2147482880
                     VALUE: last * last   === 589824
                     VALUE: first * first + last * last     === 655360
                     VALUE: (first * first + last * last)   === 655360
                     VALUE: 1973  === 1973
                     VALUE: (first * first + last * last) % 1973      === 324
                     VALUE: sz * 20000 + (first * first + last * last) % 1973       === 2147480324
D:java7_32codeoo_hmk_3_3_gbksrcmycodeMyPath.java:97: 注:        ArithmeticOperationRange assertion: |#isMul32ok#|(sz, 20000)
                     VALUE: 20000        === 20000
                     VALUE: |#isMul32ok#|(sz_2270_2270___31, 20000)   === true
D:java7_32codeoo_hmk_3_3_gbksrcmycodeMyPath.java:97: 注:        ArithmeticOperationRange assertion: |#isMul32ok#|(first, first)
                     VALUE: |#isMul32ok#|(first_2304_2304___41, first_2304_2304___41)       === false
D:java7_32codeoo_hmk_3_3_gbksrcmycodeMyPath.java:97: 注:  Invalid assertion (ArithmeticOperationRange)

这是对于mul的自动检测,可以看出问题在于乘法溢出

对规格的理解

       规格应该是一种数学的概念,就像做证明题一样。对于一些简单繁琐的方法写规格,是会显得更为具体而好实现,不过其正真有价值的地方还是对一些复杂的算法和数据结构进行另一个角度的描述。

       从作业中可以看出,最短路算法当然是用dijkstra或者floyd或者spfa等等著名的算法,没有人从规格直接出发构造数据结构和算法的。其实我是看了说明就直接上手写代码的,遇到了关于线内换乘的问题时才去查规格的,然而规格的写法的确是让人耳目一新的另一种思维视角,也许可以从中演变出一些别的算法。

       还有一点就是,规格对设计的确重要,但是它也只是对设计起辅助作用的,有时候不用陷入太深,导致寸步难行。对我来说规格最有意义的地方还是描述接口,而架构设计主要依赖于数据结构和算法中的一些基本概念,这些还是需要通过理论课的学习才能掌握的,所以还是算法为王。

原文地址:https://www.cnblogs.com/16231113-liyilun/p/10898145.html