JML是对java代码进行规格抽象的一种表达手段。
面向对象的重要原则就是过程性的思考应该尽可能地推迟。而JML可以帮助我们去靠近这个原则。其通过一些逻辑符号等表示一个方法是干什么的,却并不关心它的实现,帮助你更好的用面向对象的思想去实现代码。
JML应用工具链
使用JML编译器 可以编译含有JML标记的代码,所生成的类文件会检查JML规范。
如果程序没有遵循规范,则会抛出unchecked exception来说明程序违背哪一条规范。
JML工具有JML运行期断言检查编译器、Jmldoc、jmlunnit等,其中jmlunit可以生成一个java类文件测试的框架,可以方便得使用Junit工具测试含有JML标记的java代码。
部署SMT Solver
因为windows下用命令行真是太不方便了,我直接在IDEA中部署了openJML,官方github下载openjml,解 压,然后导入IDEA
Check-->Run Check即可运行
以MyPath为例:
/*D:\Mine\hw10\src\MyPath.java:12: 警告: The prover cannot establish an assertion (PossiblyNegativeIndex) in method MyPath
nodes.add(nodeList[i]);*/
public MyPath(int... nodeList) {
for (int i = 0; i < nodeList.length; i++) {
nodes.add(nodeList[i]); //error!
/* 这是典型的下标可能为负的错误
* 需要增加 i >= 0 的判断
*/
nodesset.add(nodeList[i]);
}
}
/*D:\Mine\hw10\src\MyPath.java:49: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: D:\Mine\openjml\openjml.jar(specs/java/lang/Object.jml):76: 注: ) in method equals
if (nodes.size() == ((Path) obj).size()) {*/
if (nodes.size() == ((Path) obj).size()) { //error!
/* 这是一种无异常判断的错误
* 这里需要增加异常判断
*/
return true;
} else {
return false;
}/*D:\Mine\hw10\src\MyGraph.java:15: 警告: The prover cannot establish an assertion (NullField) in method MyGraph
以MyGraph为例
/*D:\Mine\hw10\src\MyGraph.java:15: 警告: The prover cannot establish an assertion (NullField) in method MyGraph
private HashMap<Integer, Path> idToPath = new HashMap<>();*/
private HashMap<Integer, Path> idToPath = new HashMap<>();
//new HashMap<>()中<>补全
/*D:\Mine\hw10\src\MyGraph.java:301: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method addEdge: overflow in int sum
count++;*/
count++;
//加法溢出
/*D:\Mine\hw10\src\MyGraph.java:91: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: D:\Mine\hw10\src\com\oocourse\specs2\models\PathContainer.java:57: 注: ) in method addPath
if (path == null || !path.isValid()) {*/
if (path == null || !path.isValid()) { //error
//增加异常处理
return 0;
}
部署JMLUnitNG/JMLUnit
同样,我没有成功尝试命令行,仍然选择在IDEA中导入jmluniting.jar
(1) 在IDEA中下载TestMe
(2) project structure-->modules-->导入jmluniting.jar
(3) 对应类上alt+Ins --> test --> testme --> testNG
public class test { private int a; private int b; public test(int a1, int b1) { a = a1; b = b1; } public int add() { return a + b; } public int sub() { return a - b; } public int mul() { return a * b; } }
生成测试代码
import org.testng.Assert; import org.testng.annotations.Test; public class testTest { test test = new test(0, 0); @Test public void testAdd() { int result = test.add(); Assert.assertEquals( result, 0); } @Test public void testSub() { int result = test.sub(); Assert.assertEquals(result, 0); } @Test public void testMul() { int result = test.mul(); Assert.assertEquals(result, 0); } } //Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme
运行
三次作业架构与迭代设计
第一次作业
主要是设计Path和 PathContainer
对于Path,观察JML发现:
1、Path中储存了可重复的nodes,因此可以采用可装入可重复元素容器:数组、arraylist等。
2、Path具有统计不同nodes的功能,可以考虑使用HashSet来进行存储。
对于PathContainer,观察JML发现:
1、该类所操作的主要数据是id-->Path 的映射,且是一种双射。基于映射的特性,可以采用HashMap来存储。
2、对于双射:id-->Path,要求有双向查找功能,即需要实现函数和反函数的两种查找功能。又因为其是双射,两个HashMap来进行存储无疑是可行且高效的。
3、HashMap的key需要注意其compareTo接口,对于Path,需要我们重写hashCode()。
注意:重写hashCode()要注重效率,绝不能采用直接return 1,一定要有所区分,尽可能区别。
4、与Path类似,这里同样需要统计不同Path的个数。简单的遍历必然会造成TLE。可以采用<path, count>的形式储存,直接统计其size()即可。把复杂度交给add和remove。
第二次作业:
主要是实现Graph。
对于图,无疑需要考虑两点:
1、数据结构
图需要考虑的是边和点。
对于点,
可以直接沿用上一次的存储方式。
对于边,
(1)大概率是个稀疏无向图,采用邻接矩阵不合适。
(2)回忆DS课关于稀疏矩阵的存储方式——邻接表。
(3)如何存邻接表?我们需要尽可能选择一种好的存储形式和容易来帮助我们快速操作边(这里包括已知from,搜索to、便于增、删、查.....)
(4)基于(3)可以考虑构造映射起点-->所有终点+出现次数(便于删除)。
(5)基于(4),HashMap来做映射再好不过了。
2、图算法
主要涉及两种算法和一种机制
(1)图的连通性算法
无论是dfs、还是bfs、还是并查集,个人感觉复杂度的差距不大。但是我更喜欢并查集+优化路径压缩,因为并查集维护起来个人感觉比较方便,查找也比较快捷,原因在于
**并查集是若干个集合(连通块),能够实现较快的合并和判断元素所在集合的操作。(听起来就非常适用本次的情况??)
**查找复杂度O(1),合并复杂度o(h)(h为合并后树的高度)
**union的复杂度可以分担给addPath
并查集学习链接https://blog.csdn.net/johnny901114/article/details/80721436
(2)最短路径算法
Dijsktra? or Flord?
我选择Dij,原因在于Flord在我的认知里是有点浪费的。每次都求出所有点,最短路径,万一用不到呢?万一又被add、remove了呢?但是大量数据+少量图结构修改的前提下,选择Flord好像也不错。
(3)Cache机制
其实很简单,就是把已经算过的无论是最短路径还是连通性,存储已经计算好的结果。(滚雪球)
下次调用时,先查询cache,后计算,防止重复计算。
修改图结构,需要清空cache。
第三次作业
这是一次让人头疼的作业。
与前面大有不同的就是类似最短路径的算法增加了换乘的消耗。也就是weight(1-->1) == 0 || weight(1-->1) != 0
拆点是种好方法,但点太多容易炸内存,也容易炸时间。
因此,选择合适的拆点方法是重中之重。
直接强拆:
点抽象成<node, pathId>。增加node1 == node2 && pathId1 != pathId2 的边的权值
巧妙拆:
来自讨论区的想法。每个点拆成站台+起点+终点
个人实现了这两种拆点方法。
先写了强拆的方法,发现1000+指令就有点受不了??不知道是不是cache机制做的不够好?
后来临时补了巧妙拆的方法,时间上有了提升,但最后提交截至前出现bug没有解决,无奈交了第一版。
最后果然被炸的体无完肤。
bug情况
第一次作业:
这是一次比较简单的作业,但是由于我考虑不周,出现了TLE的问题。
主要问题在于
(1)hashCode()重写过于简单,直接return1
(2)没有双HashMap配合,导致搜索复杂度过高
第二次作业:
本次作业没有发现bug
第三次作业:
本次作业由于巧妙拆点的方法在最后出现了bug,没有完成修复,最后提交了第一版强拆点的,导致了TLE
心得体会
面向对象的思想要求我们关注和设计每个类和方法实现的效果,推迟中间过程化的思考。规则撰写非常有利于我们去遵循这个原则,JML帮助我们在设计层面更加规范化得思考每个类和方法的状态,表达逻辑严谨,又能给后续编码带来好的指导。
规则撰写非常有助于理清思路,也有助于延展性的思考,对个人非常有帮助。