JML单元作业博客


作业详情:

V9OWyq.md.png

1.1 梳理JML语言的理论基础

在JML的学习中我主要通过指导书和第一次作业中助教提供的链接学习,经过整理如下:

0. 前言

What JML?

JML语言本身多用在严谨的软件工程领域内,达到需求与规格描述的准确的目的。
JML引入了大量用于描述行为的结构,比如有 模型域、量词、断言可视范围、预处理、后处理、条件继承以及正常行为(与异常行为相对)规范等等

Why JML?

使用JML来声明性地描述一个方法或类的预期行为可以显著提高整体的开发进程。把建模标记加入到你的Java程序代码中有以下好处:

  1. 能够更为精确地描述这些代码是做什么的
  2. 能够高效地发现和修正程序中的bug
  3. 可以在应用程序升级时降低引入bug的机会
  4. 可以提早发现客户代码对类的错误使用
  5. 可以提供与应用程序代码完全一致的JML格式的文档

JML和Junit、JMLUintNG等测试模块有着密切的联系,这里引用zsa学长的讲解是:

JML的一大意义在于为Junit提供设计(,从而可以起到分离功能开发人员与测试人员的效果,估计表现在工程开发的顺序上):

  1. 充分完善需求
  2. 根据需求产出设计规格(利用JML语言或自然语言等)
  3. 工程实现阶段
    3.1. 功能模块的编写
    3.2. 测试模块的编写
  4. 测试

从中我们可以清晰地看出JML语言在实际开发中的优势,因为在实际开发的过程中为了保证开发程序与功能的完备性与正确性,我们需要对工程进行充分地测试,而JML语言等规格描述语言就使得编写人员与测试人员能够分离地同步进行,一来效率上升,二来相互不影响。

此外,JML语言等规格设计语言对于程序的编写人员来说也是一大福音,因为规格描述可以讲工作任务甚至是责任划分到每个人的头上,不会出现因为甲乙双方的描述不清或语义模糊等人为情况导致最后功能上的缺陷,使得责任分担更加明确。

how JML?
  • 思维上的转变:在JML的学习中,我们不是被压迫剥削的码农!而是咸鱼翻身的 产品经理,我们只管提要求,我需要你怎么做,做出来要怎么样!比如我需要你使用 高端大气上档次的PS,而不是 lowB的photoshop,最后给我做出一个 五彩斑斓的黑。因为我是产品经理,所以我其实什么都不会,所以我对中间怎么实现的过程完全不 care,我给你原来的模板,你给我修改后的产品就ok。
  • 忘掉你的方法:比如电梯载人的问题,产品经理并不知道什么是容器,并不知道请求是如何被放进容器的,他只知道载人=请求被放在容器里面,所以我们在使用 JML 语言的时候,使用的应该是 pure 的方法,描述的结果是不对任何东西进行改动,比如有的人在描述中直接写了 HandleList.add(req) 这种语句,而这个语句是实现层面的语句,他不是 pure 的,这个语句如果被执行的话 HandleList 会发生改变,会增加一个元素,这不是我们希望的描述。正确的描述应该是这样的:
/*@
@......
@ensures (HandleList.size() == \old(HandleList.size()));
@ensures (HandleList.contains(req));

即载人后容纳的人数应该比原来多1,且新的请求在队列中。

1. 形式

JML存在于 Java 文档的注释中,所以完全不会对代码的编译和运行造成影响,具体表现形式主要是在注释中每行以 @ 开头的 JML 语法,如:

/* @
@ public normal_behavior
@ requires ! isEmpty();
@*/
Object pop() throws NoSuchElementException;

多行注释时用 /**/ 的形式,单行注释可以使用 //@ 的形式。

进行描述的都是表达式,即返回值是布尔型的值,一个很容易犯的错误是:将相等写成 = ,因为 JML 在运行前不进行语法检查,所以有时候比较难发现。

2. 作用域

JML 和 Java 一样有 private-protected-、 以及 package- 级别的作用域,作用与 Java 相同。通常我们使用的都是 public-

3. 前置条件 (requires)

前置条件表示调用一个 方法前 必须满足的一些要求,即该 代码运行前 必须是符合要求的,合法的。如要在栈中要调用出栈函数,前提条件是 栈中有数 ,所以这里我们的前置条件是:requires !stack.isempty()

4. 后置条件 (ensures)

后置条件表示调用一个 方法后 必须满足的一些要求,即代码运行后必须是符合我们所 期望的。比如在栈中要调用出栈函数,后置条件是 取得的数是原来的栈的最顶端的数栈存储的数比原来少1 ,所以我们这里的后置条件是:

/*@
@......
@ensures \result == \old(stack.get(stack.size()-1));
@ensures stack.size() == \old(stack.size()-1);

5. 模型域 (model)

模型域类似于 行为规范中的成员变量,如:

//@ public model instance JMLObjectBag elementsInQueue;

创立了一个公开的类型为 JMLObjectBag 的,名称为 elementsInQueue 的模型实例。
三个需要注意的地方:

  • instance 关键字告诉我们了:虽然这个模型是在接口中被定义出来的,但是 每个实现接口的类 都可以有一个 单独的、非静态的 模型域。
  • 因为这个模型域也是 JML 中的用法,同样在注释中声明,所以还是和 Java 代码是无关的,即 真实运行的代码中不存在这个模型域,不能引用
  • 单独构建一个包在每次操作时进行检查会让人觉得效率低下,但是因为行为规范中 不涉及代码实现 ,所以行为规范只是在描述行为接口,规定使用接口的客户代码所能依赖的外部行为。而在代码实现部分可以使用任意 满足行为规范的 高效的数据结构。
  • (续上条)虽然在定义行为规范的时候我们不考虑效率问题,但是如果开启 JML 中的断言选项的时候是要考虑的,所以开启断言检查会增加程序运行的压力。

样例:

/*@
@ public normal_behavior
@ requires ! isEmpty();
@ ensures
@ elementsInQueue.equals(((JMLObjectBag)
@ /old(elementsInQueue))
@ .remove(/result)) &&
@ /result.equals(/old(peek()));
@*/
Object pop() throws NoSuchElementException;

注:这里其实和我们前文所说的使用 pure 的方法并不那么矛盾,因为我们并没有对真正代码实现做任何非 pure 的改动,相反地,我们在行为规范中新建了模型来指导具体的代码实现

模型域的取值 (represents)

模型域毕竟是和真正的实现域(Java代码)是严格分开的,而JML中的前置条件、后置条件和不变量都是没有副作用的,所以当我们在模型域中需要用到判定什么变量的时候,可以采取 represents 语句来沟通模型域和实现域。
如: //@ private represents isMinimumHeap <- m_isMinHeap;
就是将实现域中的 m_isMinHeap 传递给了模型域中的 isMinHeap,这样一来,当我们每次需要在模型域中用到 isMinHeap 这个变量的时候,我们都会从实现域中的 m_isMinHeap 同步一下,获得真正的当前的变量然后再进行判断之类的操作。
再如:

/*@ private represents elementsInQueue
@ <- JMLObjectBag.convertFrom(
@ Arrays.asList(m_elements)
@ .subList(1, m_size + 1));
@*/

就是将模型域中的 elementsInQueue 和实现域中的 m_elements[1,m_size] 联系了起来,每当模型域中要用到 elementsInQueue 就会和 m_elements 同步。 JMLObjectBag.convertFrom 是转换类型的静态函数,他将传入的数组转换为 elementsInQueue 所需要的 JMLObjectBag

6. 不变量 (invariant)

不变量是要求表达式在 可见状态下 表达式为真。可见状态描述的是 完整可见的状态,考虑他的对立面比较方便,不可见有点像 在变化中,不稳定 的意思,我们在对象变化的时候所捕捉的状态都是不稳定的,即这里的不可见的,现在我们再来理解就简单一些,下面的都是 可见状态

  1. 对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻
  2. 在调用一个对象回收方法(finalize方法)来释放相关资源开始的时刻
  3. 在调用对象o的非静态、有状态方法(non-helper)的开始和结束时刻
  4. 在调用对象o对应的类或父类的静态、有状态方法的开始和结束时刻
  5. 未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻
  6. 未处于对象o对应类或者父类的静态方法被调用过程中的任意时刻

样例:

//@ public instance invariant elementsInQueue != null;

这里表示我们生成的实例 elementsInQueue可见状态下 不为null,其中最直接的影响就是上面可见状态的第一条:对象的有状态构造方法(用来初始化对象成员变量初值)的执行结束时刻不为null,所以构造的时候就要初始化为有意义的值。

7. 约束 (constrain)

主要用于描述delta,变化量的约束,如增删操作 Math.abs(a.length - \old(a.length)) <= 1

8. 运行时检查 (repOK)

上面所说的不变量和约束都是静态的限制,当我们自己在编写程序的时候我们就应该实现这种检查,具体来说就是使用 无参数的返回值为布尔类型的 repOk() 方法
repOk 就是实现不变量的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值