本单元的三次作业均为根据JML规格要求编写代码。三次作业依次为实现Path类和PathContainer类,扩展PathContainer类为Graph类完成部分简单图操作,最后为扩展Graph类为RailwaySystem类已完成不同含义的最短路查询。
本文将从JML理论基础及其工作链,部署JML UnitNG并自动生成测试,梳理架构设计,和分析代码实现的bug和修复情况来对OO第三单元进行总结,最后还将奉上对规格攥写和理解上的拙见。
1 JML理念梳理
1.1 理论基础
JML是用于对Java程序进行规格化设计的一种表示语言,是一种行为接口规格语言,基于Larch方法构建,BISL提供了对方法
和类型的规格定义手段。
其在实际设计中的主要作用为 不拘泥于程序猿编写代码的具体实现形式,抽象出当前方法的需要实现的功能,而且并不像自然
语言那样容易具有二义性,精确制导表达代码实现的功能。
主要使用场景为
-
- 编写代码前精确传导接口需要实现的功能,完成架构人员和具体代码实现人员的“灵魂契合”,避免出现“心领神不会”,然后甩锅的现象。
- 维护代码时精确表达实现功能,借助JML精确的表达性,在进行代码维修工作时,可以很快了解代码实现与规格的不同点,或者找到规格和预期设计的不同点。
1.2 JML工作链
因为笔者这部署了OpenJml和JML UnitNG,我就简单借用其来简单阐述应用工作链吧。
-
- 通过JML编译器(我这里使用的是OpenJml) 编译含有JML注释的代码。(对了,建议添加 -check 选项对生成的文件进行JML规格检查(你会打开一个新的天地))
- JML UnitNG可以生成一个Java类文件测试的框架,基于JML并结合Openjml的
-rac
运行时检查选项,实现对代码的自动化测试。(我认为对代码的自动测试好像大多是基于数据类型的边界轰炸)。 - JML_doc工具可以生成含有JML规格的HTML格式文档。
2 部署JML UnitNG并自动生成测试
3 梳理框架设计
第一次作业
任务简要说明
本次作业,需要完成的任务为实现两个容器类Path
和PathContainer
,学习目标为JML规格入门级的理解和代码实现。
Path类接口要求存储该路径中的所有结点,而且并实现一些常用的方法。如 iterator(),compareTo()。
我这边使用ArrayList来存储节点,其中为了应对 getDistinctNodeCount() ,我会在第一次调用该方法的时候,new一个hashset容器,其后直接返回其size。(仔细看了提供的规格,发现不能修改一条路的结点序列后,才敢头硬这么做的)PathContainer类功能主要为添加,删减path,实现path和pathid的双映射。其中为了完成Path与PathId的双映射,我这边选用俩个hashmap,从而实现该功能并且都是O(1)的复杂度。(好像直接使用俩个Arraylist会超时)。
对了,为了实现PathContainer的getDistinctNodeCount(),我又使用了一个hashmap来记录结点和其出现次数。(key为一个node值,而value为该node出现的次数)。就是为了维护该容器,我们需要在添加,删除path的时候,完成对该hashmap的维护。
第一次作业大致框架
第二次作业
第二次作业相对第一次作业,就是扩展了PathContainer类为Graph类(一个由诸多结点,路径组成的无向图系统),并增加了一些图的基础操作。
为了保存这个图,我选择使用
private HashMap<Integer, HashMap<Integer, Integer>> subGraph;
第一个key为node值,value则是另一个hashmap,这个hashmap的key为node相邻结点的node值,而value,则是记录这个边出现的次数。
这样我就保存了图这个结构
但是,我的代码结构很不好,我没有让我的Graph类继承第一次作业的PathContainer类,而是把PathContainer的代码粘贴过来,导致总体架构不是很好看。
至于第二次作业的重要点
public boolean isConnected(int fromNodeId, int toNodeId); public int getShortestPathLength(int fromNodeId, int toNodeId);
我采用BFS来实现.(对于无权图,BFS的效率可以说是极其高了,就是可扩展性不佳)
但是这里面有一个需要注意的点
就是如果你偷懒在实现isConnected()方法时,直接调用了getShortestPathLength()方法的话,你就必须注意到俩个方法JML规格的不同点。
第二次作业架构
第三次作业
第三次作业相比于第二次对图的操作逐渐复杂起来。而且为了更好完成功能和业务分离,我另外构建了GraphFuction类。把与图有关的计算全部放到该图类中进行。
相比第二次作业,第三次作业也仅仅增加了4个方法,但这四个方法中每一个方法都需要为其设计专门的存储结构。
我首先解决的是,最小联通块问题,直接使用BFS染色来判断联通块数量。
下面三个设计到权重的最小值问题,在经过了讨论区大佬的建议,我选择统一抽象为一个最短路问题,只是权重设置不一样罢了。
为了应对换乘,不满意度,票价的“换乘问题”,我采用讨论区zyy大佬的算法,进行拆点操作,把每个点拆解为2+x个点,其中x为该点出现在不同路的次数。感到zyy大佬的算法很好,抽象出起点和终点,把问题更加实体化,把权重设置变得更加容易,还减少了边的数量。膜
具体实现
-
- 首先为了区分不同路的相同结点,我采用了 结点值+“ ”+PathId来形成一个字符串来解决。(好的框架应该选择封装node类,来应对以后的扩展)
- 为了使用邻接矩阵这种结构来存储图,我需要完成结点值(int 范围)的映射,选择一个hashmap来完成。(空间换时间)
- 封装求解邻接矩阵方法来生成三个图。按照zyy大佬的思路,我只需要传入合适的普通边(结点和结点之间的权)(抽象出的终点到起点的权)(拿最小换乘来举例的话,结点之间权为0,终点到起点的权为1)
- 选择合适的最短路求解算法,我选择Dijkstra算法。
- 对了节约时间,我们还需要记录已经求解出的最短路。(只需要在图更新的时候,舍弃重新记录即可)
第三次作业框架如下
4:bug分析
- 第一次作业
第一次作业的错误很狗血。
就是int爆限的问题,真的没想到经过大一各种“好题”训练的我,还会栽在这个地方。
- 第二次作业
公测,互测倒是没有发现什么bug。
自己私下发现了一些
-
- 如果你在实现isConnected()中想调用getShortestPathLength(),一定要注意俩者JML规格的不同。即在调用getShortestPathLength()抛出异常的时候,捕捉这个异常,并返回false
- 在使用hashmap自带的get()或者remove()方法时,切记需要保证hashmap中包含该元素。或者,准备捕捉异常。
- 第三次作业
记住在直接调用父类的方法时候,要保证父类算法实现的结构和功能符合你的要求,要不然可能你会像我一样想哭。
5 规格撰写与理解的心得体会
- 使用JML规格的主要原因就是为了避免自然语言的二义性。这样,在多人协作开放项目的时候,就可以很好地避免由于对方法,接口理解歧义而导致的开发方向错误。但是,尽管通过使用JML规格可以保证编写的严谨性,但是与此同时,也不得不说,JML的描述有时候过于繁琐,为了简化表达,可能需要进行拆分功能。这样的话,感到无论是对规格编写者还是理解者都是一件要求门槛比较高的任务。因此就我看来,在大工程开发时候可以使用JML规格来保证理解的准确性,但是在小团体开发的话,还是使用自然语言比较好
- 就是合理设计自己代码的框架。可以很清楚的看出我们每单元的三次作业的递增性很明显。如果从一开始设计的时候,就设计一个比较好的代码框架,在后面进一步开发中就可以事半功倍。我想,这也是OO课所一直提倡的关键点吧。