---恢复内容开始---

[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
}

(三)特点:

  1. 跳过方法的实现,直接描述方法的功能
  2. 规范化的注释,并且能够被自动检测正确性

(四)用法:

具体使用详见《JML Level 0手册》

  1. 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规

    格。
  2. 针对已有的代码实现,书写对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

(五)工具链:

  1. 使用到IDEA的OpenJML插件,检查JML是否符合规格gitHub下载地址

    openJML。
  2. 使用JUnit生成测试样例,进行单元化测试

二、部署SMT Solver并验证

很遗憾,电脑不兼容openJML,按照教程严格执行,却一直失败:

  1. 下载时出错:

  2. 配置时出错:

    用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具有以下规范:
    1. 若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。
    2. 如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。
    3. 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原则:面对需求,对程序的改动是通过增加新代码进行的,而不是改变原来的代码。)

(二)算法设计

本次作业比较关键的几个方法为:

  1. MyPathContainer.getDistinctNodeCount()
  • 不能在每次接到指令DISTINCT_NODE_COUNT时都重新计算一遍,所花的时间复杂度是不能承受的。最开始我就是直接遍历,导致第一次强测爆40
  • 需要有一个图来存每一个节点信息:
private HashMap<Integer, Integer> nmap = new HashMap<>();
//node的容器<node,number(node出现在各个路径中的次数)>
  • 每次加入或删除路径时对其更新
  • 接到指令DISTINCT_NODE_COUNT时直接返回nmap.size();
  1. 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,算法较为简单
  1. 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的过程如下:
    1. 初始化:nextSet=new Node(0,0,null);nextSet.add(new Node(node,0,null));
    2. 将nextSet的首节点移出设置为arrived
    3. 遍历arrived连接的所有边并加入nextSet:for(Integer edgeHashCode: linkMap.get(arrive.getNode()))
    4. 根据arrived更新对应的最小单元图
    5. 跳回第二步,直到nextSet为空集
  • 事后考虑到v=|node|<=120,而e=|edge|<=4000,实际上o(v^3)优于o(v(v+e)),使用floyed算法的效率要比dijstra效率高,事实也是如此,并且floyed算法要比dijstra简便很多

(三)代码复杂度分析

  1. 依赖关系
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

均在正常范围内

  1. 方法复杂度(显示超标)
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过程中不可避免碰到了如下问题:
  • 构造一个简单的测试用例(前面进行了测试)



    如果严格按照之前的过程,会出现一个意想不到的问题:
  1. 遍历node=1,nextSet: (2,0,1/2)
  2. 遍历node=2, nextSet: (3,0,2/3)->(4,1,2/4)
  3. 遍历node=3, nextSet: (5,1,3/5)->(4,1,2/4)
  4. 遍历node=5, nextSet: (4,1,2/4)->(6,2,5/6) //->(4,2,4/5)
  5. 遍历node=4, nextSet: (6,2,5/6) //->(5,1,4/5)
  6. 遍历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规格模式的更多相关文章

  1. 规格模式(Specification Pattern)

    本文节选自<设计模式就该这样学> 1 规格模式的定义 规格模式(Specification Pattern)可以认为是组合模式的一种扩展.很多时候程序中的某些条件决定了业务逻辑,这些条件就 ...

  2. SE_Work3_结队项目

    项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求:求交点个数 结对项目作业 班级:005 Sample GitHub地址 intersect 北航网盘地址 SE结队项目 1. PS ...

  3. 规范模式-------From ABP Document

    介绍 规范模式是一种特定的软件设计模式,通过使用布尔逻辑 (维基百科)将业务规则链接在一起,可以重新组合业务规则. 在实际中,它主要用于 为实体或其他业务对象定义可重用的过滤器. 例 在本节中,我们将 ...

  4. [转]DDD领域驱动设计基本理论知识总结

    领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...

  5. Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

  6. 自定义控件(View的绘制流程源码解析)

    参考声明:这里的一些流程图援引自http://a.codekk.com/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7% ...

  7. DDD领域驱动设计基本理论知识总结

    领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...

  8. 分享我对领域驱动设计(DDD)的学习成果

    本文内容提要: 1. 领域驱动设计之领域模型 2. 为什么建立一个领域模型是重要的 3. 领域通用语言(Ubiquitous Language) 4.将领域模型转换为代码实现的最佳实践 5. 领域建模 ...

  9. 转:MediaCoder H.264格式编码参数设置及详解

    转: http://mediacoder.com.cn/node/81 由于现在大部分视频转码都选择H.264格式进行编码,同时CUDA编码的画质还达不到x264软编码的质量(如果你对画质无要求,可以 ...

随机推荐

  1. JVM之调优及常见场景分析

    JVM调优 GC调优是最后要做的工作,GC调优的目的可以总结为下面两点: 减少对象晋升到老年代的数量 减少FullGC的执行时间 通过监控排查问题及验证优化结果,可以分为: 命令监控:jps.jinf ...

  2. 在SSM框架中如何将图片上传到数据库中

    今天我们来看看SSM中如何将图片转换成二进制,最后传入到自己的数据库中,好了,废话不多说,我们开始今天的学习,我这里用的编辑器是IDEA 1.导入图片上传需要的jar依赖包 1 <depende ...

  3. springBoot高级:自动配置分析,事件监听,启动流程分析,监控,部署

    知识点梳理 课堂讲义 02-SpringBoot自动配置-@Conditional使用 Condition是Spring4.0后引入的条件化配置接口,通过实现Condition接口可以完成有条件的加载 ...

  4. Typora标题自动编号+设定快捷键技巧

    Typora标题自动编号 提示:要了解将这些CSS片段放在哪里,请参阅添加自定义CSS. 打开Typora偏好设置,打开主题文件夹,在主题文件夹中创建base.user.css文件,放置以下内容,则T ...

  5. Hadoop学习笔记—Yarn

    目录 一些基本知识 ResourceManager 的恢复 Resource Manager的HA YARN Node Labels YARN Node Attributes Web Applicat ...

  6. 201871030107-常雅伦 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告

    项目 内容 课程班级博客链接 班级博客 这个作业要求链接 作业要求 我的课程学习目标 1.体验软件项目开发中的两人合作,练习结对编程(Pair programming).2.掌握Github协作开发程 ...

  7. MyBatis-Plus日常工作学习

    一:Mybatis-Plus概述 MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis ...

  8. Dynamics CRM的Associate功能

    Dynamics CRM有一种特殊的关联关系叫Associate,一般常见于为用户分配角色.给团队添加用户.团队添加角色.队列添加用户等等.在一些特定场景下我们不可能把所有的操作都通过手动来完成尤其是 ...

  9. Netcore webapi action swagger response返回参数使用匿名类型

    问题:action中返回匿名对象时,swagger只能按强类型生成返回值描述 解决办法:使用roslyn在内存中动态执行代码,使用json.net反序列化匿名对象,向swagger返回动态匿名对象 效 ...

  10. 【笔记】《Redis设计与实现》chapter15 复制

    15.1 旧版复制功能的实现 同步 命令传播 旧版复制功能的缺陷 15.3 新版复制功能的实现 Redis2.8开始,使用PSYNC命令替代SYNC命令来执行复制时的同步操作 PSYNC命令具有完整重 ...