OO_Unit3_JML规格模式
---恢复内容开始---
[CSDN博客链接](https://blog.csdn.net/weixin_43387647/article/details/90451173)
@[toc]
## 一、JML语言的理论基础及应用工具链
#### (一)定义:
JML(Java Modeling Language):对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言 (Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义
手段。所谓接口即一个方法或类型外部可见的内容。
#### (二)契约式设计核心思想:
软件系统中的元素之间相互合作以及“**责任**”与“**义务**”
1. 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的**先验条件**—客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况。
2. 保证退出时给出特定的属性:这就是函数的**后验条件**—供应商的义务,显然也是客户的权利。
3. 在进入时假定,并在退出时保持一些特定的属性:**不变式**。
method A(){
assert(前置条件)
do something
assert(后置条件)
return
}
(三)特点:
- 跳过方法的实现,直接描述方法的功能
- 规范化的注释,并且能够被自动检测正确性
(四)用法:
具体使用详见《JML Level 0手册》
- 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规
格。 - 针对已有的代码实现,书写对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
(五)工具链:
- 使用到IDEA的OpenJML插件,检查JML是否符合规格gitHub下载地址
openJML。 - 使用JUnit生成测试样例,进行单元化测试
二、部署SMT Solver并验证
很遗憾,电脑不兼容openJML,按照教程严格执行,却一直失败:
- 下载时出错:

- 配置时出错:
用IDEA导入包时不能识别
三、JMLUnitNG/JMLUnit自动生成测试用例
(一)对Edge类的hashCode进行测试
- 源码:
public int hashCode() {
int[] array = new int[3];
array[0] = min;
array[1] = max;
array[2] = pathId;
return Arrays.hashCode(array);
}
- 测试:
@Test
public void hashCodeTest() {
assertNotEquals(new Edge(-69,54,1).hashCode(),
new Edge(-68,23,1).hashCode());
//Values should be different. Actual: -34843
HashSet<Integer> set = new HashSet<>();
for (int i = -50; i < 50; i++)
for (int j = -50; j < 50; j++) {
Edge edge = new Edge(i, j, 1);
set.add(edge.hashCode());
}
assertEquals(set.size(), 5050);
//java.lang.AssertionError:
// Expected :2704
// Actual :5050 i!=j: (100+1)*100/2
}
- 结果:发现此hashCode生成不能满足需求,使得两个不同的边hashCode的值不同
- hashCode与equals具有以下规范:
- 若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。
- 如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。
- Java运行时环境是怎样判断HashSet和HastMap中的两个对象相同或不同:先判断hashcode是否相等,再判断是否equals。
- 修正:
反hash(希望建立一个hashCode到Edge本身一一对应的映射)的前提:任何两个同类型不同的对象的哈希值不同
public int hashCode() {
return (min + "," + max + "," + pathId).hashCode();
}
(二)针对Graph接口的测试用例
@Test
public void getLeastTransferCountTest() {
Path path = new MyPath(1,2,3);
MyRailwaySystem system = new MyRailwaySystem();
system.addPath(path);
path = new MyPath(2,4,5,6);
system.addPath(path);
path = new MyPath(3,5);
system.addPath(path);
try {
assertEquals(system.getLeastTransferCount(1,6),1);
//java.lang.AssertionError:
//Expected :2
//Actual :1
} catch (Exception e) {
e.printStackTrace();
}
}
根据说明信息,发现在dijstra更新过程中出现错误,在bug修复处分析了具体原因及解决方式。
四、架构设计
(一)类图设计
本单元作业要求依次实现四个类:
- 使用Path接口的MyPath
- 使用PathContainer接口的MyPathContainer
- 使用Graph接口的MyGraph
- 使用RailwayStation接口的MyRailwayStation
其中,MyPathContainer是MyPath的容器,并且实现了MyGraph的部分功能。因此,MyGraph中以MyPathContainer作为组成元素之一。同理,MyRailwayStation将MyGraph作为组成元素之一。各类包含关系如下:

在后续迭代过程中,不对前面实现过的类进行修改,仅是扩展添加新的属性及方法(遵循OCP原则:面对需求,对程序的改动是通过增加新代码进行的,而不是改变原来的代码。)
(二)算法设计
本次作业比较关键的几个方法为:
- MyPathContainer.getDistinctNodeCount()
- 不能在每次接到指令
DISTINCT_NODE_COUNT时都重新计算一遍,所花的时间复杂度是不能承受的。最开始我就是直接遍历,导致第一次强测爆40 - 需要有一个图来存每一个节点信息:
private HashMap<Integer, Integer> nmap = new HashMap<>();
//node的容器<node,number(node出现在各个路径中的次数)>
- 每次加入或删除路径时对其更新
- 接到指令
DISTINCT_NODE_COUNT时直接返回nmap.size();
- MyGraph.getShortestPathLength(int fromNodeId, int toNodeId)
- 使用到BFS:一种单元扩张的搜索方式。其适用的前提是保证value严格递增。
- 并且BFS的时间复杂度对单源是
o(v+e),对全图搜索复杂度为o(v(v+e)) - 需要有一个邻接链表存储点之间关联关系(使用到键为node,值为边集合的HashMap,因此还需要设一个建立从边的hash值到边本身的映射),一个距离矩阵来存储距离信息:
private HashMap<Integer, HashSet<Integer>> linkMap;
private HashMap<Integer, HashMap<Integer, Integer>> disMap;
private HashMap<Integer, Edge> edgeMap;//hashCode(edge)-->edge(a,b)
- 其中距离矩阵是周密矩阵:第二次限制|node|<=250,第三次限制|node|<=120
- 此处存距离的disMap其实直接用矩阵存储的时空效率更高
- 接下来是对单源距离图的更新:需要fromSet, nextSet进行广度优先搜索,每次将fromSet所连接而未到达的点放入nextSet,算法较为简单
- MyRailwayStation.getLeastTicketPrice(int fromNodeId, int toNodeId)
MyRailwayStation.getLeastTransferCount(int fromNodeId, int toNodeId)
MyRailwayStation.getLeastUnpleasantValue(int fromNodeId, int toNodeId)
- 使用到dijstra方法:类似于最小生成树,是BFS的升级版,应用到邻接链表,其算法复杂度也是
o(v+e) - 用到的数据结构需要记录到达的节点,以及将要到达的节点
- 设置新的数据结构
Node,组成链表结构:是四元组<node(Integer),value(Integer),edge(Edge),next(Node)> - 正在到达的节点arrived为(next=null)的Node类型,将要到达的节点nextSet为Node类型的邻接链表,并且加入时按照value值递增
- dijstra的过程如下:
- 初始化:nextSet=new Node(0,0,null);nextSet.add(new Node(node,0,null));
- 将nextSet的首节点移出设置为arrived
- 遍历arrived连接的所有边并加入nextSet:
for(Integer edgeHashCode: linkMap.get(arrive.getNode())) - 根据arrived更新对应的最小单元图
- 跳回第二步,直到nextSet为空集
- 事后考虑到v=|node|<=120,而e=|edge|<=4000,实际上o(v^3)优于o(v(v+e)),使用floyed算法的效率要比dijstra效率高,事实也是如此,并且floyed算法要比dijstra简便很多
(三)代码复杂度分析
- 依赖关系
| Class | Cyclic | Dcy | Dcy* |
Dpt | Dpt* |
|---|---|---|---|---|---|
| Edge | 0 | 0 | 0 | 3 | 5 |
| Main | 2 | 2 | 6 | 2 | 3 |
| MyGraph | 2 | 3 | 6 | 1 | 3 |
| MyGraphTest | 0 | 0 | 0 | 0 | 0 |
| MyPath | 0 | 0 | 0 | 2 | 4 |
| MyPathContainer | 0 | 0 | 0 | 1 | 4 |
| MyRailwaySystem | 2 | 4 | 6 | 2 | 3 |
| MyRailwaySystemTest | 0 | 2 | 7 | 0 | 0 |
| Node | 0 | 1 | 1 | 1 | 4 |
均在正常范围内
- 方法复杂度(显示超标)
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| MyGraph.isConnected(int,int) | 4 | 2 | 5 |
| MyGraph.nodeEdgeOk(int) | 4 | 3 | 4 |
| MyRailwaySystem.getLeast(int,int,int) | 5 | 3 | 5 |
| MyRailwaySystem.renewNodeMap(int,int) | 3 | 13 | 14 |
| Node.add(Node) | 5 | 6 | 6 |
其中MyRailwaySystem.renewNodeMap方法,即对于单源的dijstra方法长50行,为了兼容3个搜索,分支很多,而错误也正是出现在这里。
以后任意一个方法,行数最好不要超过30行,秉承SRP(单一职责原则),只做出一个行为,然后通过封装和组合达到最终实现行为的目的。
五、bug修复
(一)Edge类的hashCode错误
(前面已经测试过)
(二)换乘问题
- 要注意的是设有换乘,因此dijstra过程中不可避免碰到了如下问题:
- 构造一个简单的测试用例(前面进行了测试)

如果严格按照之前的过程,会出现一个意想不到的问题:
- 遍历node=1,nextSet: (2,0,1/2)
- 遍历node=2, nextSet: (3,0,2/3)->(4,1,2/4)
- 遍历node=3, nextSet: (5,1,3/5)->(4,1,2/4)
- 遍历node=5, nextSet: (4,1,2/4)->(6,2,5/6) //->(4,2,4/5)
- 遍历node=4, nextSet: (6,2,5/6) //->(5,1,4/5)
- 遍历node=6, nextSet: null
最后发现node=6处value为2,实际上value应该为1
- 解决方式:
- 在第4步,发现将要加入的(4,2,4/5)与(4,1,2/4)冲突(同node,同path,不同egde)时,加入较大者的新节点(5,1,4/5),得到nextSet: (5,1,4/5)->(4,1,2/4)->(6,2,5/6)
- 第5步:遍历node=5, nextSet: (4,1,2/4)->(6,1,5/6) (第二次遍历,注意更新)
- 第6、7步:同上5、6
(三)null指针
在HashMap数据结构中,如果访问的key值不在其中,函数会返回null,而不是抛出异常,而后对null调用函数会报错NullPointerException。因此在这里常常出现诡异的bug,以后使用get时要小心,更好的方式是使用getOrDefault,没有key值就给出默认的数据。
此外,对于HashMap套HashMap现象,连续使用两次get(),会造成意想不到的错误,不如先将乱序节点通过一个HashMap映射到从1开始的正整数上,然后直接使用二维静态数组。
六、心得体会
JML是一种先进的契约式设计模式,将设计和实现不可跨越的鸿沟大幅减小。大体上解决问题分为从需求到设计,从设计到实现两个过程。而JML规范地描述了代码需要执行的约束条件,可以说是一种LLR(Low-Level Request)。
实际上,个人感觉JML更偏向于需求,而非设计。因为不管是你需要具体的数据结构,还是算法,都需要个人亲自去定义。并且为了描述一个需求,用大段大段的代码去规范和描述也不是一个简单的任务。很可能需要多层抽象,甚至有时根本找不出一个描述需求的严格的表达方式。写规格本身都要比实现一个需求难上许多。
但是比较受用的是自动测试生成JUnit,对类中的某一个具体的函数进行测试。虽然直接在控制台输入输出也很有效,但那样测完一次就结束了,没有办法对测试集进行管理。也没有办法在扩展修改了类以后保证类的兼容性。
总之,这一个单元是相当革新的,业界缺乏成熟的代码规格化设计方式,网络上对契约式设计也鲜有听闻,但是以后可能会日渐成熟。
---恢复内容结束---
OO_Unit3_JML规格模式的更多相关文章
- 规格模式(Specification Pattern)
本文节选自<设计模式就该这样学> 1 规格模式的定义 规格模式(Specification Pattern)可以认为是组合模式的一种扩展.很多时候程序中的某些条件决定了业务逻辑,这些条件就 ...
- SE_Work3_结队项目
项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求:求交点个数 结对项目作业 班级:005 Sample GitHub地址 intersect 北航网盘地址 SE结队项目 1. PS ...
- 规范模式-------From ABP Document
介绍 规范模式是一种特定的软件设计模式,通过使用布尔逻辑 (维基百科)将业务规则链接在一起,可以重新组合业务规则. 在实际中,它主要用于 为实体或其他业务对象定义可重用的过滤器. 例 在本节中,我们将 ...
- [转]DDD领域驱动设计基本理论知识总结
领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- 自定义控件(View的绘制流程源码解析)
参考声明:这里的一些流程图援引自http://a.codekk.com/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7% ...
- DDD领域驱动设计基本理论知识总结
领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...
- 分享我对领域驱动设计(DDD)的学习成果
本文内容提要: 1. 领域驱动设计之领域模型 2. 为什么建立一个领域模型是重要的 3. 领域通用语言(Ubiquitous Language) 4.将领域模型转换为代码实现的最佳实践 5. 领域建模 ...
- 转:MediaCoder H.264格式编码参数设置及详解
转: http://mediacoder.com.cn/node/81 由于现在大部分视频转码都选择H.264格式进行编码,同时CUDA编码的画质还达不到x264软编码的质量(如果你对画质无要求,可以 ...
随机推荐
- 2018ICPC南京K. Kangaroo Puzzle
题目:在一个20×20的地图上,1表示有袋鼠,0表示有障碍物,边界外和障碍物上不能走. 要求给出一个50000步以内的操作,每一步操作为'L', 'R', 'U', 'D', 表示所有袋 ...
- 2018ICPC 南京Problem J. Prime Game
题目: 题意:给出一个序列a1,⋯,ana1,⋯,an.fac(l,r)fac(l,r)为mul(l,r)mul(l,r)中不同质因数的个数. 请计算: ∑i=1n∑j ...
- DenseNet的个人总结
DenseNet这篇论文是在ResNet之后一年发表的,由于ResNet在当时引起了很大的轰动,所以DenseNet也将ResNet作为了主要的对比方法,读起来还是比较容易的,全篇只有两个数学公式,也 ...
- 翻译:《实用的Python编程》06_03_Producers_consumers
目录 | 上一节 (6.2 自定义迭代) | 下一节 (6.4 生成器表达式) 6.3 生产者,消费者和管道 生成器在设置各种生产者/消费者问题(producer/consumer problems) ...
- Java高并发测试框架JCStress
前言 如果要研究高并发,一般会借助高并发工具来进行测试.JCStress(Java Concurrency Stress)它是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JV ...
- [图论]最短网络:kruskal
最短网络 目录 最短网络 Description Input Output Sample Input Sample Output 解析 代码 Description 农民约翰被选为他们镇的镇长!他其中 ...
- BUAA_OO_第一单元
BUAA_OO_2020_UNIT1 一.程序结构分析 第一次作业 UML & Mertrics 由于数据处理简单,第一次作业中笔者发挥了面向过程的思想,将项转换成Biginteger, ...
- Day14_82_反射机制输出整个类
利用反射机制输出整个类 代码实例 import java.lang.reflect.Field; import java.lang.reflect.Modifier; public class Ref ...
- linux gcc命令参数
gcc命令参数笔记 1. gcc -E source_file.c -E,只执行到预处理.直接输出预处理结果. 2. gcc -S source_file.c -S,只执行到汇编,输出汇编代码. 3. ...
- 二. 简单初步认识SpringCloud
(一)微服务的实现方式很多,但是最火的莫过于Spring Cloud了.为什么? 后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大. 技术强:Spring作为Java领域 ...