应用建模语言JML改进Java程序.docx
- 文档编号:7067868
- 上传时间:2023-01-16
- 格式:DOCX
- 页数:13
- 大小:26.41KB
应用建模语言JML改进Java程序.docx
《应用建模语言JML改进Java程序.docx》由会员分享,可在线阅读,更多相关《应用建模语言JML改进Java程序.docx(13页珍藏版)》请在冰豆网上搜索。
应用建模语言JML改进Java程序
应用建模语言JML改进Java程序
寿智振
Java建模语言(JavaModelingLanguage,JML)是一种进行详细设计的符号语言,它鼓励你用一种全新的方式来看待Java的类和方法。
面向对象的分析和设计(OOAD)的一个重要原则就是过程性的思考应该尽可能地推迟,不过遵循这个原则的大多数人也不过是把这个原则适用到方法实现这个级别上。
一旦设计好了类和接口,下面的事情自然就是实现其中定义的方法了。
对呀,我们还能做什么呢?
还有什么其它方法可以使用吗?
毕竟,用Java进行程序设计和用其他语言进行程序设计一样,我们都要一步一步地实现每一个方法。
标记本身只是表示如何做一个事情(howtodosomething),根本不管我们希望做什么。
如果我们在做一个事情之前就能够知道我们能够达到什么样的结果是非常好的,不过Java语言并没有给我们提供一个可以显式地把这些信息插入到我们程序代码的方法。
Java建模语言(JavaModelingLanguage,JML)在Java代码中增加了一些符号,这些符号用来标识一个方法是干什么的,却并不关心它的实现。
如果使用JML的话,我们就能够描述一个方法的预期的功能而不管他如何实现。
通过这种方式,JML把过程性的思考延迟到方法设计中,从而扩展了面向对象设计的这个原则。
JML引入了大量用于描述行为的结构,比如有模型域、量词、断言可视范围、预处理、后处理、条件继承以及正常行为(与异常行为相对)规范等等。
这些结构使得JML非常强大,不过你并不必要理解或者使用上面所述的所有方面,也不需要一次使用所有的这些方面。
这篇文章中采用循序渐进的方式来介绍JML。
我们要先了解一下使用JML的各种好处,特别是对开发和编译过程的影响。
然后,我们要讨论一下JML的一些结构,比如前置条件、后置条件、模型域、量词、副作用以及异常行为等等。
同时,在讨论这些结构的同时,我们会给出一些例程来给你一个直观的感觉。
这样经过本文你将可以对JML是如何工作的有一个概念性的理解,从而能够在你自己的项目中应用JML。
一、Java建模语言JML对Jave程序代码的改进
使用JML来声明性地描述一个方法或类的预期行为可以显著提高整体的开发进程。
把建模标记加入到你的Java程序代码中有以下好处:
∙能够更为精确地描述这些代码是做什么的
∙能够高效地发现和修正程序中的bug
∙可以在应用程序升级时降低引入bug的机会
∙可以提早发现客户代码对类的错误使用
∙可以提供与应用程序代码完全一致的JML格式的文档
JML标记总是在Java注释的内部,所以对正常编译的代码没有任何影响。
如果你想比较一下普通的类和使用了JML的类有什么差别的话,你可以使用一个开源的JML编译器。
用JML编译器编译的代码如果没有实现JML规范所要求的事项,运行时就会抛出一个JML异常。
这个特性不仅可以帮助我们捕获代码中的bug,而且可以确保JML形式的文档可以与程序代码高度一致。
使用开放源代码的JakartaCommonsCollectionComponent(JCCC)项目中的PriorityQueue接口和BinaryHeap类来演示JML的各种性质。
在这里你可以找到使用了JML标记完整的这个两个文件。
1、确定类实现的规范
本文中使用的代码包括开源项目JCCC中的PriorityQueue接口。
接口嘛,自然是声明了一些方法的签名,包括方法的参数类型、返回值的类型,并不涉及方法的实现。
一般情况下或者只是按照Java语法要求的话,实现接口的类只要实现了接口中定义的各个方法即可,不论实现的方式是多么地离奇古怪。
我们并不想这样,我们希望能够确定一个行为规范,所有实现这个接口的类都用我们指定的方式来实现这个接口中定义的方法。
通过使用JML我们可以做到这一点。
考虑一下PriorityQueue接口的pop()方法,对于优先级队列来说,pop()方法应该有什么样的功能要求?
最起码应该有三个:
第一,如果要调用pop()方法,队列中至少要有一个元素;第二,该方法应该返回队列中优先级最高的那个元素;第三,该方法应该从队列中删除返回的那个元素。
下面代码段显示了表示满足第一个要求的JML标记:
代码段1pop()方法功能规范的JML标记
/*@
@publicnormal_behavior
@requires!
isEmpty();
@*/
Objectpop()throwsNoSuchElementException;
前面已经提到,JML标记是写在Java代码的注释中的。
包含JML标记的多行注释以/*@开头,JML忽略任何以@开头的空行。
如果是单行的话,你也可以使用//@这种标记。
这里JML注释中public关键字与Java中的public意思是一样的,它表示程序中其他所有的类都要遵循这个JML要求。
Public要求只能应用在public方法和public成员变量上。
JML同样有private-、protected-、以及package-级别的作用域。
同样,这些作用域的规则与Java语言中作用域的规则非常相似。
这里normal_behavior关键字的意思是,这个JML要求表示这是一种正常情况,运行时不会抛出异常。
后面,我们会描述异常行为是怎么被界定的。
2、前置条件和后置条件
JML关键字requires用来表示前置条件,前置条件表示调用一个方法前必须满足的一些要求。
上面代码段中包含一个前置条件,它要求调用pop()方法的前提就是isEmpty()方法返回false,也就是说要求这个队列至少含有一个元素。
一个方法的后置条件规范表示一个方法的责任,也就是说当这个方法返回时,它必须满足这个后置条件的要求。
在我们上面的例子中,pop()方法应该返回队列中优先级最高的元素。
我们希望指定一个后置条件要求JML在运行时检查是否满足这个事实。
要做到这一点,我们必须跟踪所有添加到这个优先级队列中的元素,这样我们就可以判断pop()方法应该返回哪一个元素。
怎么做呢?
你可能会考虑在PriorityQueue接口中加入一个成员变量来存储队列中元素的值,不过这样做有两个问题:
∙PriorityQueue是一个接口,它可能有各种不同具体的实现方式,比如说binaryheap、Fibonacciheap或者calendarqueue等等,它要与它的各种实现一致,况且JML标记不应该涉及到任何具体的实现细节。
∙作为一个接口,PriorityQueue只能拥有静态成员变量。
为了处理这种情况,JML引入了一个叫做模型域(modelfields)的概念。
3、模型域
模型域类似于成员变量,它只能被应用到行为规范中。
这是一个PriorityQueue中声明模型域的例子:
//@publicmodelinstanceJMLObjectBagelementsInQueue;
这个声明的意思是说这里有一个叫做elementsInQueue的模型域,它的类型是JMLObjectBag(这个数据类型是在JML中定义的)。
instance关键字表示虽然这个域是定义在接口中,可是任何实现这个接口的类都拥有一个单独的、非静态的elementsInQueue域。
与其他JML标记一样,这个声明也是出现在注释中的,所以常规的Java代码是不能使用这个elementsInQueue变量的。
在程序运行的时候,是没有任何对象拥有一个叫做elementsInQueue的成员变量的。
elementsInQueue存储添加到优先级队列的元素的值,下面的代码段显示pop()方法如何使用elementsInQueue:
代码段2在pop()的后置条件中使用模型域
/*@
@publicnormal_behavior
@requires!
isEmpty();
@ensures
@elementsInQueue.equals(((JMLObjectBag)
@\old(elementsInQueue))
@.remove(\result))&&
@\result.equals(\old(peek()));
@*/
Objectpop()throwsNoSuchElementException;
ensures关键字表示后面跟着的是pop()方法返回时必须满足的后置条件。
\result是一个JML关键字,它等于pop()方法的返回值。
\old()是一个JML函数,它返回pop()方法调用之前参数的值。
这个ensures语句包含了两个后置条件。
第一,pop()方法返回的那个元素必须要从elementsInQueue删除。
第二,这个返回值要与peek()方法返回的值一致。
不过虽然用JML定义行为规范的时候不需要考虑执行效率,程序运行时JML断言检查却是很重要的。
所以开启断言检查时程序的运行可能会有性能的压力。
4、行为规范与实现
使用一个包来存储队列中的元素,然后检查每一个元素找出优先级最高的那一个会让人觉得效率不高。
不过这只是行为规范的一部分,而不会涉及到实现。
行为规范的作用在于描述PriorityQueue的行为接口,也就是说规定了使用PriorityQueue的客户代码所能依赖的外部行为。
PriorityQueue接口的各个具体实现只要可以满足这个行为规范的要求,就可以使用任何更为高效的方法。
比如说,JCCC有一个实现这个接口的BinaryHeap类,它的实现方式就是使用一个存储在数组中的binaryheap。
5、类级别的不变量
我们现在已经看到JML能够让我们规定方法的前置条件和后置条件,它同样也允许我们指定类级别的不变量。
类级别的不变量指的是进入和退出一个类中每个方法都必须满足的条件。
比方说吧,//@publicinstanceinvariantelementsInQueue!
=null;就是PriorityQueue的一个不变量,它的意思是任何实现PriorityQueue的类一旦被实例化,elementsInQueue的值就不能是null。
6、量词(Quantification)
这里量词的意思与逻辑学上的量词意思相近,而不是普通意义上理解的量词。
在上面pop()方法的行为规范中,我们说它的返回值要等于peek()方法的返回值,不过我们并没有看到关于peek()方法的规范。
PriorityQueue中peek()方法的行为规范请看下面的代码:
代码段3PriorityQueue中peek()方法的行为规范
/*@
@publicnormal_behavior
@requires!
isEmpty();
@ensureselementsInQueue.has(\result);
@*/
/*@pure@*/Objectpeek()throwsNoSuchElementException;
JML标记要求只有当队列中至少含有一个元素的时候,才能调用peek()方法,同时他还要求方法的返回值必须在elementsInQueue之内,也就是说,这个返回值一定是这个队列中的一个元素。
注释/*@pure@*/表明peek()方法是一个纯方法(puremethod),纯方法是指没有副作用的方法。
JML中只允许使用纯方法进行断言确认,所以我们把peek()声明为纯方法,这样我们就可以在pop()方法的后置条件中使用peek()方法。
大家肯定想知道,为什么JML只允许使用纯方法进行断言确认?
问题是这样的,如果JML允许使用非纯方法进行断言确认的话,我们稍不注意就会写出有副作用的行为规范。
比如说可能会有这么一种情况,开启了断言确认以后,我们的代码正确无误,可是如果禁止了断言确认后,我们的代码却不能运行了,或运行出错了。
这样当然不行!
后面,我们还会进一步讨论副作用的问题。
7、关于继承
JML行为规范可以被子类(含子接口)或者是实现接口的类所继承,这一点与J2SE1.4中断言有所不同。
JML关键字also表示当前定义的行为规范与祖先类或被实现的接口中所定义的行为规范一起作用。
因而,在PriorityQueue接口定义的peek()方法的行为规范同样适用于BinaryHeap类中的peek()方法。
这个就意味着,虽然在BinaryHeap.peek()的行为规范中没有明确定义,BinaryHeap.peek()的返回值也必须在elementsInQueue当中。
8、大顶堆和小顶堆
大顶堆和小顶堆是数据结构里面的概念,分别表示堆排序方法的不同实现方式。
堆排序是一种通过调整二叉树进行排序的方法。
上面我们给peek()定义的行为规范明显缺少了一块,那就是我们根本没有要求它返回的那个元素具有最大的优先级。
显然,JCCC的PriorityQueue接口既可以用于大顶堆,也可以用于小顶堆。
大顶堆和小顶堆的表现是有些差别的,在小顶堆中优先级最高的元素值最小,而大顶堆中优先级最高的元素值最大。
因为PriorityQueue并不知道自己被用来进行大顶堆排序还是小顶堆排序,所以指定返回哪个元素的规范必须在实现PriorityQueue接口的类中进行定义。
在JCCC中,类BinaryHeap实现了PriorityQueue接口。
BinaryHeap允许使用它的客户代码在构造函数中通过一个参数来指定排序方案,也就是通过参数来指定是通过大顶堆方式排序还是通过小顶堆方式排序。
我们使用一个boolean模型变量isMinimumHeap来判断BinaryHeap的排序方式是大顶堆还是小顶堆。
下面的例子是BinaryHeap使用isMinimumHeap给peek()方法定义的行为规范:
代码段4BinaryHeap类中peek()方法的行为规范
/*@
@also
@publicnormal_behavior
@requires!
isEmpty();
@ensures
@(isMinimumHeap==>
@(\forallObjectobj;
@elementsInQueue.has(obj);
@compareObjects(\result,obj)
@<=0))&&
@((!
isMinimumHeap)==>
@(\forallObjectobj;
@elementsInQueue.has(obj);
@compareObjects(\result,obj)
@>=0));
@*/
publicObjectpeek()throwsNoSuchElementException
9、使用量词
上面代码段4中的后置条件包含两个部分,分别用于大顶堆和小顶堆的情况。
“==>”符号的意思是包含(译者注:
这个包含与逻辑学中包含的意思一致)。
x==>y当且仅当y为真或x为假时取真值。
对于小顶堆排序来说,适用下面所列的代码:
(\forallObjectobj;
elementsInQueue.has(obj);
compareObjects(\result,obj)<=0)
上面的代码中\forall是一个JML量词。
上面\forall表达式当所有的对象obj满足elementsInQueue.has(obj)为真且compareObjects(\result,obj)返回一个非正数两个条件时才为真。
换言之,当使用compareObjects()进行比较时,peek()方法的返回值一定小于或等于elementsInQueue中每一个元素的值。
其他的JML量词还有\exists、\sum以及\min等等
10、使用Comparator进行比较
BinaryHeap类允许以两种方法比较元素的大小。
一种方法是要进行比较的元素自己实现Comparable接口,比较过程使用元素中定义的方法进行。
另外一种方法是客户类在构造BinaryHeap类的实例时向构造函数传入一个Comparator对象,使用该Comparator对象进行比较。
无论使用哪一种比较方式,我们都使用模型域comparator来表示比较大小所用的Comparator对象。
在peek()方法的后置条件中,compareObjects()方法使用客户端选择的比较方式来进行元素的比较。
compareObjects()方法定义如下:
代码段5compareObjects()方法
/*@
@publicnormal_behavior
@ensures
@(comparator==null)==>
@(\result==((Comparable)a).compareTo(b))&&
@(comparator!
=null)==>
@(\result==pare(a,b));
@
@publicpuremodelintcompareObjects(Objecta,Objectb)
@{
@if(m_comparator==null)
@return((Comparable)a).compareTo(b);
@else
@returnm_pare(a,b);
@}
@*/
compareObjects方法的定义中使用了另外一个关键字model,它的意思是compareObjects方法是一个模型方法。
模型方法是只能用在行为规范中的JML方法。
模型方法定义在Java的注释中,所以常规的Java代码不能使用。
如果BinaryHeap类的客户代码指定了一个特殊的Comparator用来进行比较的话,m_comparator就指向那个Comparator,否则m_comparator的值就是null。
compareObjects()方法检查m_comparator的值,然后采用适当的方法进行元素间的比较。
11、模型域如何取值
在代码段4中我们讨论了peek()方法的后置条件。
这个条件保证peek()方法的返回值的优先级大于或者等于模型域elementsInQueue中所有的元素的优先级。
那么有一个问题,像elementsInQueue这样的模型域如何取值?
前置条件、后置条件和不变量都是没有副作用的,所以不能使用它们来设置模型域的值。
JML使用一个represents语句把模型域与具体的实现域关联起来。
比如下面的represents语句用来给模型域isMinimumHeap赋值:
//@privaterepresentsisMinimumHeap<-m_isMinHeap;
这个语句的意思是模型域isMinimumHeap的值等于m_isMinHeap的值,其中,m_isMinHeap是BinaryHeap类中一个私有的布尔变量。
一旦需要用到isMinimumHeap的值,JML就会把m_isMinHeap的值赋给它。
represents语句并没有限制<-右边必须是成员变量。
比如说,下面是elementsInQueue的represents语句:
代码段6elementsInQueue的represents语句
/*@privaterepresentselementsInQueue
@<-JMLObjectBag.convertFrom(
@Arrays.asList(m_elements)
@.subList(1,m_size+1));
@*/
从这里我们可以看出,elementsInQueue的元素就是数组m_elements[]从第一个元素到第m_size个元素共m_size个元素构成的列表,其中数组m_elements[]是BinaryHeap的一个私有成员,用来存储优先级队列中的元素,m_size是m_elements[]中正在使用的元素的个数。
类BinaryHeap没有使用m_elements[0],这样可以简化对于索引的操作。
JMLObjectBag.convertFrom()的作用是把一个List结构转化为一个elementsInQueue所需要的JMLObjectBag结构。
这样一旦JML运行时进行断言检查的时候需要elementsInQueue的值,系统就会计算represents语句<-符号右边的代码并进行求值。
二、副作用
1、副作用产生的原因:
大家回忆一下代码段2中pop()方法的后处理代码:
ensures
elementsInQueue.equals(((JMLObjectBag)
\old(elementsInQueue))
.remove(\result))&&
\result.equals(\old(peek()));
这里我们说有一个副作用,那就是在从elementsInQueue删除一个元素的时候会有副作用。
事实上,这里还可能有其他的副作用。
比方说,一个pop()方法的具体实现中如果修改了m_isMinHeap的值,那么就把排序方法从一个小顶堆变成了大顶堆。
只要这种修改能够返回正确结果,就不会引起运行期间的断言检查异常,可是这个却事实上削弱了JML行为规范的作用。
我们可以加强后置条件,不允许除了修改elementsInQueue以外的任何改变,请看下面的代码:
代码断7加强的后置条件
ensures
elementsInQueue.equals(((JMLObjectBag)
\old(elementsInQueue))
.remove(\result))&&
\result.equals(\old(peek()))&&
isMinimumHeap==\old(isMinimumHeap)&&
comparator==\old(comparator);
从中我们可以看出,通过加入形如x==\old(x)的语句,我们可以消除变量x发生改变的副作用。
可是有一个问题,如果用这种办法,每一个方法在它的后置条件都要为每一个变量加上这么一句,这样就会导致行为规范的混乱。
而且如果我们给一个类增加一个成员的变量的话,那么我们就得在这个类的所有方法的后处理规范中增加一句,这将让维护变得异常困难。
2、解决方法:
JML通过引入assignable语句提供了一种更好地解决方案。
使用assignable语句,我们可以这样完成pop()方法的后置条件:
代码段8在方法的行为规范中使用assignable语句
/*@
@publicnormal_behavior
@requires!
isEmpty();
@assignableelementsInQueue;
@ensures
@elementsInQueue.equals(((JMLObjectBag)
@\old(elementsInQueue))
@.remove(\result))&&
@\result.equals(\old(peek()));
@*/
Objectpop()throwsNoSuchElementException;
只有在assignable语句中列出的变量才能在一个方法的实现中修改。
上面pop()方法的assignable语句的
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 应用 建模 语言 JML 改进 Java 程序
![提示](https://static.bdocx.com/images/bang_tan.gif)