JML 理论基础与工具链情况
-
理论基础
- JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口(即一个方法或类型外部可见的内容)规格语言(Behavior Interface Specification Language,BISL),提供了对方法和类型的规格定义手段。
- 整体感觉 JML 类似于数理逻辑的表达方式,JML 原子表达式(old, esult 等)相当于一些预先定义好了的谓词(max, sum 这些也相当于一种谓词),结合全称、特称量词以及一些逻辑运算符(类似于数理逻辑中,蕴含、等价这些关系)来完成对代码行为的约束描述。
-
工具链情况
-
openJML 作为 Eclipse 用户在 openJML 官网上能够找到 eclipse pulgin 的链接,通过 eclipse 在线安装功能能够非常简单的安装好 openJML 插件,并能够整合 SMT Solvers 进行静态与运行状态下的 JML 检查。
- 启用 openJML JML 语法检测之后,发现不能够对 esult 进行赋值来表示结果是什么,而应该使用判断式表明结果是什么。
-
- 在修改了上述 bug 之后,使用 SMT Solver 进行静态检查(ESC)以及运行时检查(RAC),并无异样。
```
Completed proof of jml.TryJML.compare(int,int) with prover cvc4 - no warnings [0.29 secs]
Completed proving methods in jml.TryJML [1.81 secs]
Summary:
Valid: 2
Invalid: 0
Infeasible: 0
Timeout: 0
Error: 0
Skipped: 0
TOTAL METHODS: 2
Classes: 1 proved of 1
Model Classes: 0
Model methods: 0 proved of 0
DURATION: 2.1 secs
[3.00] Completed
[0.00] Executing openjml on TryJML.java
RAC-Compiling jml.TryJML
[0.56] Completed
```
JMLUnitNG
- 对之前写的 .java 进行 JMLUnitNG 自动测试结果如下。
[TestNG] Running:
Command line suite
Passed: racEnabled()
Failed: constructor Demo()
Failed: static compare(-2147483648, -2147483648)
Failed: static compare(0, -2147483648)
Failed: static compare(2147483647, -2147483648)
Failed: static compare(-2147483648, 0)
Failed: static compare(0, 0)
Failed: static compare(2147483647, 0)
Failed: static compare(-2147483648, 2147483647)
Failed: static compare(0, 2147483647)
Failed: static compare(2147483647, 2147483647)
Failed: static main(null)
Failed: static main({})
===============================================
Command line suite
Total tests run: 13, Failures: 12, Skips: 0
===============================================
架构设计
-
第一次作业设计思路
- MyPath 通过利用 HashSet 元素不重复的性质来解决统计路径内不同节点的个数,而 MyPathContainer 使用 HashMap 来实现这一功能(需要记录一个点出现了多少次,方便删除的时候)。
- 对于加速 MyPathContainer 的查询速度,采用了 Path -> Id, Id -> Path 的双 hashMap 的数据结构实现。
-
第二次作业设计思路
- 对于如何存储图结构,采用使用一个 hashMap<Link, Integer> 来记录图中所有边与其出现的次数,然后使用同样的哈希结构来存储距离矩阵,使用 floyd 暴力计算每个点到每个点的最短距离。
-
第三次作业设计思路
-
对于计算最少票价、最小不满意度,采用讨论区大佬的建模思路,先计算出一条路径中各个点之间的最小票价(这时不需要考虑换乘,使用 floyd),然后将所有结果加上一次换乘的代价后整合到一个矩阵之中作为初始票价矩阵(采用 HashMap 作为具体实现),然后就可以愉快的 dijsktra。
- 在这过程中,发现 floyd、dijsktra 这些图通用算法没有必要为每一个具体的功能都写一个实现(只是边权重计算不一样而已),所以可以实现一个接口(WeightCal),然后分别实现具体问题的边权重计算函数,到时候作为一个参数传入即可。
// 接口类 public interface WeightCal { // use for calculate different weight value public int weightCal(int fromNode, int toNode); } // 具体票价换乘代价实现 public class TicketWeight implements WeightCal { public TicketWeight() { } // 用于计算同一条路径上个点之间的最小票价所以没考虑换乘 public int weightCal(int fromNode, int toNode) { return 1; } }
-
对于最少换乘采用了广度优先的遍历算法,我想的是最少换乘只需要遍历路径树,而不需要具体遍历每一条边,这样就不需要用 dijsktra 那么麻烦的实现。简单来说:只需要计算出一条路径到哪些路径是可达的,然后从起始点所在的路径集合开始按广度优先的方式进行遍历,就能够很简单的实现(不过在 Java 容器的选择上被坑了,实属尴尬)。
-
bug 分析
- 第一次作业并未被发现 bug。
- 第二次作业中,遇到了 CPU_TLE 的 bug,经过分析发现主要是因为写的 Link 类(边类)重写的 hashCode 性能过差。我认为是 Objects.hashCode 的方法过于复杂,导致我每次查询 hashMap 的时候都要花大量时间计算 hash 值。本地测试下来,更改之前 7000 条指令大概要跑 30 多秒,更改之后基本在 10 s 之内就能够跑出来了。
@Override
public int hashCode() {
return Objects.hashCode(node1, node2);
}
// 修改之后的 hashCode
@Override
public int hashCode() {
if (node1 < node2) {
return Integer.hashCode(node1) * 31 + Integer.hashCode(node2);
} else {
return Integer.hashCode(node2) * 31 + Integer.hashCode(node1);
}
}
- 第三次作业中,最小换乘中出现了一个小 bug,因为采用的广度优先算法,所以需要一个队列,而我选择的是 java 自带的 PriorityQueue 作为我的队列,但我错误的认为默认的优先队列是按照时间顺序来着的,但后面发现不是,在 bug 修复的时候换成 LinkedBlockingQueue 就 OK 了,真是失误。
心得体会
-
JML 规格方面
- 总体来讲,JML 规格有点像用数学表达式(离散数学中的数理逻辑)来严格规范类方法的行为,通过这种没有二义性的语言来约束项目内代码实现的一致性。
- 但是感觉这三次作业要实现的接口实现类功能单靠函数名字就能够很清楚的看出来了,只有在考虑边界条件的时候(如,fromNode == toNode)才会去切实查看 JML 规格。所以三次作业下来,对于 JML 的切实感触只停留在看得懂语法,写过一点 JML,感觉应用不是很深。
- 其次,在上一次企业讲座中,老师提到了他对于 PDL 的看法,私以为 JML 也是一种 PDL,感觉只要代码功能说清楚,也不一定需要写 JML。
-
Junit 体验
-
因为这三次作业功能单元都相比之前的电梯非常独立,很适合单元测试。感觉 Junit 具有如下优点:
- 能够专门测试一些边界数据、特殊情况。如,讨论区中提出了一个直接 Dijsktra 的反例,然后就可以把图数据记录下来,写到代码里面,然后加上 assert 语句,轻松验证自己以及其他人的代码是否存在这种问题。
@Test public void dijkstraTest() { MyRailway rail = new MyRailway(); rail.addPath(new MyPath(1, 2, 3, 4, 5)); rail.addPath(new MyPath(1, 6, 7)); rail.addPath(new MyPath(7, 5, 8)); try { assertEquals(6, rail.getLeastTicketPrice(1, 8)); } catch (NodeIdNotFoundException | NodeNotConnectedException e) { e.printStackTrace(); } }
- 一次编写 Junit 很简单的重复测试(run 一下就行),很适合后面优化后自己的代码能够通过之前的测试样例。
-