OO第9-11次作业总结
JML单元总结博客
1、设计分析
1.1 第九次作业--实现路径类和路径容器类
摸鱼划水?!就是一个简单的增删查问题。不过注意,涉及统计的方法,需要做均摊。因为,增删方法调用较少,把计算任务分配到这些方法中,即可减少平均时间复杂度。
巧用多重容器,会带来意想不到的效果。另外,善用Hash容器,因为它们平均增删查时间复杂度均为O(1)。
UML类图
复杂度分析
方法
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| mypath.MyPath.compareTo(Path) | 6.0 | 4.0 | 7.0 |
| mypath.MyPath.containsNode(int) | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.equals(Object) | 5.0 | 3.0 | 5.0 |
| mypath.MyPath.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.getNode(int) | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.getNodes() | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.hashCode() | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.isValid() | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.iterator() | 1.0 | 1.0 | 1.0 |
| mypath.MyPath.MyPath(int...) | 1.0 | 2.0 | 2.0 |
| mypath.MyPath.size() | 1.0 | 1.0 | 1.0 |
| mypath.MyPathContainer.addPath(Path) | 3.0 | 5.0 | 6.0 |
| mypath.MyPathContainer.containsPath(Path) | 2.0 | 1.0 | 2.0 |
| mypath.MyPathContainer.containsPathId(int) | 1.0 | 1.0 | 1.0 |
| mypath.MyPathContainer.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| mypath.MyPathContainer.getPathById(int) | 2.0 | 1.0 | 2.0 |
| mypath.MyPathContainer.getPathId(Path) | 2.0 | 3.0 | 4.0 |
| mypath.MyPathContainer.MyPathContainer() | 1.0 | 1.0 | 1.0 |
| mypath.MyPathContainer.removePath(Path) | 2.0 | 5.0 | 6.0 |
| mypath.MyPathContainer.removePathById(int) | 2.0 | 2.0 | 3.0 |
| mypath.MyPathContainer.size() | 1.0 | 1.0 | 1.0 |
| Total | 37.0 | 38.0 | 49.0 |
| Average | 1.76 | 1.81 | 2.33 |
类
| Class | OCavg | WMC |
|---|---|---|
| mypath.MyPath | 2.0 | 22.0 |
| mypath.MyPathContainer | 2.1 | 21.0 |
| Total | 43.0 | |
| Average | 2.05 | 21.5 |
耦合度分析
| Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
|---|---|---|---|---|---|
| mypath.MyPath | 0.0 | 2.0 | 2.0 | 2.0 | 3.0 |
| mypath.MyPathContainer | 0.0 | 5.0 | 9.0 | 2.0 | 2.0 |
| Total | |||||
| Average | 0.0 | 3.5 | 5.5 | 2.0 | 2.5 |
复杂度、耦合度都不高,毕竟是容器类,各实现各的。
1.2 第十次作业--在路径容器的基础上,实现图算法(连通性和最短路径)
这次还是相对简单。因为,我们的图是无权图,所以实现最短路径只需要用BFS,当第一次搜索到一个点的时候就表明它已经被找到最短路径。另外,到BFS结束,仍未算出最短路,这表明这两个点不连通。所以,连通性和最短路两个方法可以合一。
另外,涉及到图的操作,所以单独封装一个无向图类,把所有的关于增加、删除边,算最短路,找顶点或边等函数封装进去。基本上无需重构,只需增加新功能。
考虑到点的ID是不连续的,所以邻接表不能用传统的邻接表,必须用嵌套HashMap。
UML类图
复杂度分析
方法
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| work.MyGraph.addPath(Path) | 3.0 | 6.0 | 7.0 |
| work.MyGraph.containsEdge(int,int) | 2.0 | 2.0 | 3.0 |
| work.MyGraph.containsNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyGraph.containsPath(Path) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.containsPathId(int) | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getPathById(int) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.getPathId(Path) | 2.0 | 3.0 | 4.0 |
| work.MyGraph.getShortestPathLength(int,int) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.isConnected(int,int) | 3.0 | 2.0 | 4.0 |
| work.MyGraph.MyGraph() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.removePath(Path) | 2.0 | 6.0 | 7.0 |
| work.MyGraph.removePathById(int) | 2.0 | 2.0 | 3.0 |
| work.MyGraph.size() | 1.0 | 1.0 | 1.0 |
| work.MyPath.compareTo(Path) | 6.0 | 4.0 | 7.0 |
| work.MyPath.containsNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.equals(Object) | 5.0 | 3.0 | 5.0 |
| work.MyPath.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| work.MyPath.getNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.getNodes() | 1.0 | 1.0 | 1.0 |
| work.MyPath.hashCode() | 1.0 | 1.0 | 1.0 |
| work.MyPath.isValid() | 1.0 | 1.0 | 1.0 |
| work.MyPath.iterator() | 1.0 | 1.0 | 1.0 |
| work.MyPath.MyPath(int...) | 1.0 | 2.0 | 2.0 |
| work.MyPath.size() | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.addEdge(int,int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.bfs(int) | 1.0 | 4.0 | 4.0 |
| work.UndirectedGraph.containsEdge(int,int) | 1.0 | 2.0 | 2.0 |
| work.UndirectedGraph.Pair.Pair(int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.putMap(int,int,int) | 1.0 | 3.0 | 3.0 |
| work.UndirectedGraph.recalculate() | 1.0 | 2.0 | 2.0 |
| work.UndirectedGraph.removeEdge(int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.removeMap(int,int) | 2.0 | 3.0 | 4.0 |
| work.UndirectedGraph.shortestPathLength(int,int) | 2.0 | 3.0 | 3.0 |
| Total | 56.0 | 66.0 | 82.0 |
| Average | 1.65 | 1.94 | 2.41 |
类
| Class | OCavg | WMC |
|---|---|---|
| work.MyGraph | 2.29 | 32.0 |
| work.MyPath | 2.0 | 22.0 |
| work.UndirectedGraph | 2.25 | 18.0 |
| work.UndirectedGraph.Pair | 1.0 | 1.0 |
| Total | 73.0 | |
| Average | 2.15 | 18.25 |
耦合度分析
| Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
|---|---|---|---|---|---|
| work.MyGraph | 0.0 | 8.0 | 16.0 | 1.0 | 1.0 |
| work.MyPath | 0.0 | 2.0 | 2.0 | 2.0 | 2.0 |
| work.UndirectedGraph | 0.0 | 1.0 | 1.0 | 1.0 | 2.0 |
| work.UndirectedGraph.Pair | 0.0 | 0.0 | 0.0 | 1.0 | 3.0 |
| Total | |||||
| Average | 0.0 | 2.75 | 4.75 | 1.25 | 2.0 |
1.3 第十一次作业--在上次的图基础上,模拟地铁系统
这是我OO生涯的第二次翻车。可能和时间太紧张有关系,两次OO作业离得太近。不过,大部分原因在我自己,太过想当然,并查集都不去专门检查。不出所料,所有WA的点全部在这上面。细节决定成败。
不过,在讨论区的指导下,设计思路得到了极大的拓展。我开始甚至想用分支限界,后来一想,这是指数级别的。有大佬提示我用拆点,我就想到拆成完全图,结果超时超的一塌糊涂。最后,zyy奆佬想到了”站台模式“,豁然开朗,改完后时间缩短了70%多。
由于对并查集连通块个数考虑不周全(比如删除后怎么处理等),导致了bug。教训惨痛,永志不忘。(就要这种惨痛经历给我敲一下警钟,干啥都不能飘!)
架构上,明显的迭代开发。上次的graph类已经实现大部分功能,剩下四个方法只需要扩展这个类即可,所以直接extends,而不是复制粘贴graph类的源码。类头写成public class MyRailwaySystem extends MyGraph implements RailwaySystem。要加的东西还是不少的,还要重写add、remove方法以用于实现附加功能。但这样可避免影响之前已经完成的功能,还缩短了单个类的长度。
最短路就是迪杰斯特拉算法,优先队列优化,可以把时间复杂度降低到O(ElogE),对于稀疏图来说,比O(V^2)的一般实现要快。
UML类图
复杂度分析
方法
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| work.MyGraph.addPath(Path) | 3.0 | 6.0 | 7.0 |
| work.MyGraph.containsEdge(int,int) | 2.0 | 2.0 | 3.0 |
| work.MyGraph.containsNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyGraph.containsPath(Path) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.containsPathId(int) | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getId() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getPathById(int) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.getPathId(Path) | 2.0 | 3.0 | 4.0 |
| work.MyGraph.getPaths() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.getShortestPathLength(int,int) | 2.0 | 1.0 | 2.0 |
| work.MyGraph.isConnected(int,int) | 3.0 | 1.0 | 3.0 |
| work.MyGraph.MyGraph() | 1.0 | 1.0 | 1.0 |
| work.MyGraph.removePath(Path) | 2.0 | 6.0 | 7.0 |
| work.MyGraph.removePathById(int) | 2.0 | 2.0 | 3.0 |
| work.MyGraph.size() | 1.0 | 1.0 | 1.0 |
| work.MyPath.compareTo(Path) | 6.0 | 4.0 | 7.0 |
| work.MyPath.containsEdge(int,int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.containsNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.equals(Object) | 5.0 | 3.0 | 5.0 |
| work.MyPath.getDistinctNodeCount() | 1.0 | 1.0 | 1.0 |
| work.MyPath.getNode(int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.getNodes() | 1.0 | 1.0 | 1.0 |
| work.MyPath.getShortestPathLength(int,int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.getUnpleasantValue(int) | 1.0 | 1.0 | 1.0 |
| work.MyPath.hashCode() | 1.0 | 1.0 | 1.0 |
| work.MyPath.isValid() | 1.0 | 1.0 | 1.0 |
| work.MyPath.iterator() | 1.0 | 1.0 | 1.0 |
| work.MyPath.MyPath(int...) | 1.0 | 2.0 | 2.0 |
| work.MyPath.size() | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.addEdge(int,int,int) | 2.0 | 5.0 | 6.0 |
| work.MyRailwaySystem.addNode(int,int) | 1.0 | 2.0 | 2.0 |
| work.MyRailwaySystem.addPath(Path) | 2.0 | 2.0 | 3.0 |
| work.MyRailwaySystem.containsPathSequence(Path[]) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.dijkstra(long,int) | 4.0 | 12.0 | 16.0 |
| work.MyRailwaySystem.Edge.Edge(int,int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.getConnectedBlockCount() | 1.0 | 4.0 | 4.0 |
| work.MyRailwaySystem.getLeastTicketPrice(int,int) | 5.0 | 4.0 | 6.0 |
| work.MyRailwaySystem.getLeastTransferCount(int,int) | 5.0 | 4.0 | 6.0 |
| work.MyRailwaySystem.getLeastUnpleasantValue(int,int) | 5.0 | 4.0 | 6.0 |
| work.MyRailwaySystem.getRoot(int) | 1.0 | 2.0 | 2.0 |
| work.MyRailwaySystem.getTicketPrice(Path[],int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.getUnpleasantValue(Path,int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.getUnpleasantValue(Path,int[]) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.getUnpleasantValue(Path[],int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.isConnectedInPathSequence(Path[],int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.Length.compareTo(Length) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.Length.Length(int,long) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.mixId(int,int) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.mixId2NodeId(long) | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.MyRailwaySystem() | 1.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.removeEdge(int,int,int) | 4.0 | 3.0 | 6.0 |
| work.MyRailwaySystem.removePath(Path) | 1.0 | 2.0 | 2.0 |
| work.MyRailwaySystem.removePathById(int) | 1.0 | 2.0 | 2.0 |
| work.MyRailwaySystem.unpleasantValue(int,int) | 2.0 | 1.0 | 2.0 |
| work.UndirectedGraph.addEdge(int,int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.bfs(int) | 1.0 | 4.0 | 4.0 |
| work.UndirectedGraph.containsEdge(int,int) | 1.0 | 2.0 | 2.0 |
| work.UndirectedGraph.Pair.Pair(int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.putMap(int,int,int) | 1.0 | 3.0 | 3.0 |
| work.UndirectedGraph.removeEdge(int,int) | 1.0 | 1.0 | 1.0 |
| work.UndirectedGraph.removeMap(int,int) | 2.0 | 3.0 | 4.0 |
| work.UndirectedGraph.shortestPathLength(int,int) | 3.0 | 4.0 | 4.0 |
| Total | 107.0 | 128.0 | 160.0 |
| Average | 1.70 | 2.03 | 2.54 |
类
| Class | OCavg | WMC |
|---|---|---|
| work.MyGraph | 2.0625 | 33.0 |
| work.MyPath | 1.79 | 25.0 |
| work.MyRailwaySystem | 3.05 | 67.0 |
| work.MyRailwaySystem.Edge | 1.0 | 1.0 |
| work.MyRailwaySystem.Length | 1.0 | 2.0 |
| work.UndirectedGraph | 2.43 | 17.0 |
| work.UndirectedGraph.Pair | 1.0 | 1.0 |
| Total | 146.0 | |
| Average | 2.32 | 20.86 |
耦合度分析
| Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
|---|---|---|---|---|---|
| work.MyGraph | 0.0 | 8.0 | 16.0 | 1.0 | 2.0 |
| work.MyPath | 0.0 | 2.0 | 7.0 | 2.0 | 3.0 |
| work.MyRailwaySystem | 0.0 | 9.0 | 20.0 | 1.0 | 1.0 |
| work.MyRailwaySystem.Edge | 0.0 | 0.0 | 0.0 | 1.0 | 2.0 |
| work.MyRailwaySystem.Length | 0.0 | 0.0 | 0.0 | 1.0 | 2.0 |
| work.UndirectedGraph | 0.0 | 1.0 | 1.0 | 1.0 | 3.0 |
| work.UndirectedGraph.Pair | 0.0 | 0.0 | 0.0 | 1.0 | 4.0 |
| Total | |||||
| Average | 0.0 | 2.86 | 6.29 | 1.14 | 2.43 |
2、单元测试方法和JML回顾
2.1 JML
概述:JML语言是一种为Java程序设计的规格语言,遵循契约式设计规则。
编译:规格是用Java注释的方式编写的,因此可以被任意Java编译器编译。
思想:在保持可读性的同时,提供严格的形式语义。
作用:JML提供语义来严格描述Java模块的行为,防止模块设计者的设计错误。JML使用各种验证工具,例如运行时断言检查器和扩展静态检查器(ESC / Java),可帮助程序员进行开发。
`requires` pre-condition
`ensures` post-condition
`signals` exception-condition
`assignable` 定义了允许继续执行了后面方法的条件
`pure` 纯方法,不修改字段
`invariant` 定义了一个方法的不变的属性
`\result` 指向返回值
`\old(expression)` 用于表示进入方法之前表达式的值
`(\forall <decl>; <range-exp>; <body-exp>)` 全称量词
`(\exists <decl>; <range-exp>; <body-exp>)` 存在量词
`a==>b` a推出b
`a<==b` a隐含b
`a<==>b` a当且仅当b
2.2 构建一个单元测试(工具:JMLUnit)
创建如下Java文件(A-B示例)
package demo;
public class Demo {
/*@ public normal_behaviour
@ ensures \result == lhs - rhs;
*/
public static int compare(int lhs, int rhs) {
return lhs - rhs;
}
public static void main(String[] args) {
compare(114514,1919810);
}
}
生成测试类
运行一个测试
很显然,compare方法发生了溢出,所以遇到INT_MAX,INT_MIN之类的就会fail。
这就是JMLUnit工具的使用方法。总结一下:
1、生成测试类
2、编译所有的Java文件,包括测试类
3、用openjml编译自己的jml
4、运行测试
3、bug分析
前两次比较简单,没怎么出现bug。
第三次,啊,具体说说,惨烈如战场
对连通块数的统计,采用了并查集的方法。并查集就是一棵树,所以根据图论,结点数减边数等于并查集数。但是,一个致命问题:由于并查集不支持删除,所以必须在remove之后重新构造并查集。所以,我在这里犯了错误,重新构造前还可能有add,这种情况没有clear()!果不其然,强测全WA在这里。
归根结底,还是心态不对,思考的时候一心二用,又想搞定这个方法又想搞定那个方法。如果分开思考,并且做好记录,就不会出现这么弱智的bug了。另外就是算法基础薄弱。但不管怎么说,能学到东西就不算白写。
谁没犯过错误啊,这都是正常的。但是,这么优质的一次作业被我糟蹋了,感觉好心痛,唉。
再说说互测(包括发现自己bug)的方法,自己写数据生成器是硬道理!颠扑不破啊,啥时候做伸手党要生成器都是最愚蠢的。互测开始后,一对拍立刻发现自己的问题所在!
另外就是Junit。啥时候手造数据都是很弱的,只有把随机性和精确性结合起来才能地毯式轰炸。
事不过三,希望下个单元不要再有这种问题了~
4、收获
第九次,学会了写容器类,特别是深刻体会了“不要自己造轮子”的真理。另外,根据规格写功能和异常处理,体会了类的鲁棒性。
第十次,复习了一下图论算法,连通性、最短路等。补上了数据结构的短板。集思广益啊,如果不是有人提示bfs,还在用dij或floyd呢,深刻体会到贪心思想的强大。
第十一次,仍然是最短路,学会了图的奇技淫巧,包括拆点法的使用等等;又复习了dij和并查集。教训越深刻,记忆越深刻,这次闪脱的教训不仅会伴随我的oo历程,更让我对开发过程有了新的认识。
最最重要的是,对JML规格的理解使用。对规格的阅读理解可以避免很多二义性的东西,使得设计更精确。就拿containsEdge方法来说,开始没有考虑自环问题,后来提交了一次,发现错在这里,就去研读规格,避免了误解题意。
最后特别感谢伦佬提供的强大测试工具。
OO最后的斗争,加油!

浙公网安备 33010602011771号