BUAA_OO_第三单元(JML)总结
1. JML 理论基础
1.1JML表达式
1.1.1原子表达式
-
\result:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。 -
\old(expr):表示一个表达式expr在相应方法执行前的取值,该表达式涉及到评估expr中的对象是否发生变化。 -
\not_assigned(x,y,...):用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。 -
\not_modified(x,y,...):该表达式限制括号中的变量在方法执行期间的取值未发生变化。 -
\nonnullelements(container):表示container对象中存储的对象不会有null。 -
\type(type):返回类型type对应的类型(Class),如type(boolean)为Boolean.TYPE。TYPE是JML采用的缩略表示,等同于Java中的 java.lang.Class。 -
\typeof(expr):该表达式返回expr对应的准确类型。如\typeof(false)为Boolean.TYPE。
1.1.2量化表达式
-
\forall:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束 -
\exists:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。 -
\sum:返回给定范围内的表达式的和。 -
\product:返回给定范围内的表达式的连乘结果。 -
\max:返回给定范围内的表达式的最大值。 -
\min:返回给定范围内的表达式的最小值。 -
\num_of:返回指定变量中满足相应条件的取值个数。可以写成(\num_of T x; R(x);P(x)),其中T为变量x的类型,R(x)为x的取值范围;P(x)定义了x需要满足的约束条件。从逻辑上来看,该表达式也等价于(\sum T x;R(x)&&P(x);1)。
1.1.3集合表达式
可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。集合构造表达式的一般形式为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合 中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
1.1.4操作符
JML可以正常使用java所定义的操作符,此外,还专门定义了以下四类。
-
E1<:E2子类型操作符:如果类型E1是类型E2的子类型(sub type)或相同类型,则该表达式的结果为真,否则为假。任意一个类X,都必然满足X.TYPE<:Object.TYPE。 -
b_expr1<==>b_expr2或b_expr1<=!=>b_expr2等价关系操作符:其中b_expr1和b_expr2都是布尔表达式。 -
b_expr1==>b_expr2或b_expr1<==b_expr2推理操作符:相当于离散的->,只有(1,0)是false。 -
\nothing或\everthing变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。变量引用操作符经常在assignable句子中使用,如assignable \nothing表示当前作用域下每个变量都不可以在方法执行过程中被赋值。
1.2方法规格
定义前置条件和满足后置条件的东西。
1.2.1前置条件
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。 requires P;其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。多个分开的requires是并列关系都要满足,或关系用requires P1||P2;
1.2.2后置条件
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。
1.2.3副作用
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
-
从方法规格的角度,必须要明确给出副作用范围。
-
JML提供了副作用约束子句,使用关键词
assignable(表示可赋值)或者modifiable(可修改)。虽然二者有细微的差异,在大部分情况下,二者可交换使用。 -
副作用约束子句共有两种形态,
-
用JML关键词来概括,不指明具体的变量;
-
指明具体的变量列表。
-
1.3其他
-
(/*@ pure @ */)指不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。有些前置条件可以引用pure方法的返回结果 -
forall和exists前置条件或后置条件需要对不止一个变量进行约束,往往是需要对一个容器中的所有元素进行约束。 -
public normal_behavior和public exception_behavior为了有效地区分方法的正常功能行为和异常行为。如果一个方法没有异常处理行为,不必使用这两个关键词。public,指相应的规格在所在包范围内的所有其他规格处都可见。also,这里指除了正常功能规格外,还有一个异常功能规格。 -
signals (***Exception e) b_expr写好异常非常非常重要,请花大量时间学习! 强调满足某个条件抛出相应异常。 是当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e。注意一定要在方法声明中明确指出(使用Java的 throws 表达式),且必须确保signals子句中给出的异常类型一定等同于方法声明中给出的异常类型,或者是后者的子类型。signals_only (***Exception e)强调满足前置条件抛出相应异常。
1.4类型规格
类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。 从面向对象角度来看,类或接口包含数据成员和方法成员的声明及或实现。不失一般性,一个类型的成员要么是静态成员(static member),要么是实例成员(instance member)。 一个类的静态方法不可以访问这个类的非静态成员变量(即实例变量)。静态成员可以直接通过类型来引用,而实例成员只能通过类型的实例化对象来引用。因此,在设计和表示类型规格时需要加以区分。
-
invariant P不变式(invariant)是要求在所有可见状态下都必须满足的特性,其中invariant为关键词,P为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。 -
凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态。这里的可见不是一般意义上的能否见到,而是带有完整可见的意思。在会修改状态的方法执行期间,对象状态不稳定,随时可能会被修改。换句话说,在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调在任意可见状态下都要满足不变式。
参考博文:https://blog.csdn.net/piaopu0120/article/details/89527175
2. Junit的使用
Junit 是一个单元测试包,可以通过编写单元测试类和方法,来实现对类和方法实现正确性的快速检查和测试。还可以查看测试覆盖率以及具体覆盖范围(精确到语句级别),以帮助编程者全面无死角的进行程序功能测试。
本单元作业,指导书中推荐使用Junit单元测试来测试正确性。因此,我通过查阅资料,将Junit运用在本单元作业中,辅助debug,在一定程度上避免了出现bug却难以定位的情况。
一下总结这一单元我对Junit的认识。
2.1配置
配置(https://www.cnblogs.com/wangmingshun/p/6411885.html)
2.2使用
使用(https://blog.csdn.net/yitengtongweishi/article/details/80715569)
2.3断言检查
以上两篇文章简洁的讲述了Junit的配置和使用。这里我主要分享一下断言检查assertThat。
2.3.1语法
assertThat语法如下:
assertThat(T actual, Matcher<T> matcher);
assertThat(String reason, T actual, Matcher<T> matcher);
其中actual为需要测试的变量,matcher为使用Hamcrest的匹配符来表达变量actual期望值的声明;
2.3.2匹配方法
-
要求所有的条件都要通过测试才算成功
assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );
-
接下来的所有条件只要有一个成立则测试通过
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
-
无论什么条件,永远为true
assertThat( testedNumber, anything() );
-
等于判断
assertThat( testedString, is( “developerWorks” ) );
-
取反判断
assertThat( testedString, not( “developerWorks” ) );
2.3.3字符串相关匹配符
-
包含字符串
assertThat( testedString, containsString( “developerWorks” ) );
-
以指定字符串结尾
assertThat( testedString, endsWith( “developerWorks” ) );
-
以指定字符串开始
assertThat( testedString, startsWith( “developerWorks” ) );
-
字符串相等测试
assertThat( testedValue, equalTo( expectedValue ) );
-
忽略大小写判断是否相等
assertThat( testedString, equalToIgnoringCase( “developerWorks” ) );
-
忽略头尾的任意个空格的情况下等于待测字符串
assertThat( testedString, equalToIgnoringWhiteSpace( “developerWorks” ) );
2.3.4数值相关匹配符
范围测试
assertThat( testedDouble, closeTo( 20.0, 0.5 ) );
//如果所测试的浮点型数testedDouble在20.0±0.5范围之内则测试通过
大于判断
assertThat( testedNumber, greaterThan(16.0) );
小于判断
assertThat( testedNumber, lessThan (16.0) );
大于等于
assertThat( testedNumber, greaterThanOrEqualTo (16.0) );
小于等于
assertThat( testedNumber, lessThanOrEqualTo (16.0) );
2.3.5collection相关匹配符
map包含测试
assertThat( mapObject, hasEntry( “key”, “value” ) );
//hasEntry匹配符表明如果测试的Map对象mapObject含有一个键值为”key”对应元素值为”value”的Entry项则测试通过
迭代对象包含测试
assertThat( iterableObject, hasItem ( “element” ) );
//如果测试的迭代对象iterableObject含有元素“element”项则测试通过
map包含key测试
assertThat( mapObject, hasKey ( “key” ) );
//如果测试的Map对象mapObject含有键值“key”则测试通过
map包含value测试
assertThat( mapObject, hasValue ( “key” ) );
//如果测试的Map对象mapObject含有元素值“value”则测试通过
参考博客:https://blog.csdn.net/u010570551/article/details/51492649
3.设计策略
由于本单元作业只需要根据JML规格实现对应的方法,因此,基本上只需要考虑方法内部的实现。但是,由于在一个类中(特别是Network类)有数十个方法,如果对类的成员变量没有设计合理的结构,则会使得方法的实现变得很复杂,
另一方面,虽然仅要求实现对应方法,但是,实现单个方法是建立在对整个类内部所有成员变量,成员方法理解的基础上。要能够做到通过阅读JML规格,了解方法的作用,以及该方法的期望结果。
我的设计策略是自底向上,首先实现基本的异常类,接着实现Person,Group,Message类,最后实现Network类。这样做的好处在于,当我在实现Network类的方法时,我能够清楚的知道应该调用Person,Group,Message类中的哪些方法,会实现什么效果。否则,若先实现Network类成员方法,则由于对Person,Group,Message类的成员变量,成员方法模糊不清,就很有可能雾里看花,一头雾水。
另外,整个实现过程实际上是在边实现边联想,通过与日常生活中的社交软件(如微信,QQ)进行类比,很容易知道该方法的作用。
由于采用了较为合理的设计策略,因此,笔者在实现正确性上并没有遇到什么困难。
4.容器选择
针对每个类的成员变量,由于JML规格要求的均为type[]形式,因此,选择一个合适的容器就显得尤为重要。
我印象最深刻的是Network中的emojiIdList和emojiHeatList。一开始我选择依葫芦画瓢,用两个list存储,但这样设计的弊端也很明显,二者缺乏内部逻辑联系,由于idlist和heatlist是一一对应的,这里设计成HashMap更加合适,key为Id,value为Heat,这样就可以将二者紧密联系起来。
虽然从表面上看只是容器上的选择问题,但是,选择一个适合的容器,往往能够达到事半功倍的效果。
另一方面,选择合适的容器在性能上也有很大的提升。
arraylist的查找性能为O(n),而hashmap的查找为O(1),很明显性能有了大幅提高。这对于第一次作业的isCircle方法,第三次作业的sendIndirectMessage方法尤为明显。
isCircle方法我选择用并查集实现。使用set比list性能明显提高。
sendIndirectMessage方法我选择用PriorityQueue,使得性能提高到O(nlogn),而选择list,性能为O(n^2),强测中会出现CTLE。
5.性能分析及bug 情况
由于官方代码给出了架构框架,因此只需要根据JML完善类的方法即可。在完善某个方法时,只需要考虑该方法内部实现,这从一定程度上加深了我对面向对象的认识。
5.1 第一次作业
第一次作业的难点在于Network中isCircle方法的实现。
因为这里会卡性能,一开始我选择dfs来实现,但觉得每次都需要从头搜索,性能难免降低,因此我改进为用并查集来解决,因为Java有丰富的数据结构作支持,因此,并查集的思路就自然而然。
遇到ap指令,则对该person新建一个set,加入list中,遇到ar指令,则将ar连接的两个person所在的set合并为一个set,当需要判断isCircle时,只需要判断person1和person2是否在同一个set中即可,对于qbs指令,只需要返回set的数量即可。
通过上述优化,第一次作业我并没有被hack到bug。
5.2 第二次作业
第二次作业难点在于Group中getValueSum、getAgeMean、getAgeVar方法的实现。
由于没有考虑性能,我直接按照JML给出的方法规格来实现,用了O(n^2)的算法,导致在强测中出现了CPU_TIME_LIMIT_EXCEED的bug。
解决方法:在Group中用成员变量记录每次addPerson和delPerson,在遇到addRelation时,对于包含person1和person2的所有group,更新ValueSum即可。
5.3 第三次作业
第三次作业难点在于sim指令,抽象来说就是用dijkstra算法求最短路径问题。
由于没有考虑堆优化,性能上为O(n^2),被hack到bug。
而采用优先队列的堆优化后性能可以优化为O(nlogn)。值得注意的是,在上一次查找完毕后,需要用map保存查询结果,这样,在没有更新点或边时,再次sim时可以直接O(1)读出,无需重复查询。
6.架构设计
6.1图的构建
Person类相当于点,relation相当于边,value相当于边的权重,由于是无向图,因此Person类中用hashmap(key: id, value: value)存储了与该点相连的边及权重(acquaintance)。在Network类中有所有点(Person)的信息(用List存储)。这样就把图给构建起来.
6.2图的维护
ap(addPerson)指令新建一个点,保存在Network的personList中,ar(addRelation)指令则将两边以value权重相连,通过增加person1与person2中的acquaintance的键值对维护了图的完备性。
6.3相关算法
6.3.1并查集
isCircle与queryBlockSum方法的实现基于并查集。
-
ap指令则新建一个set,set中只有该点 -
ar指令则将person1和person2所在的set合并成一个新的set
isCircle:判断person1和person2是否在同一个set(同一个set内所有的点必然相连)
queryBlockSum:返回set 的个数。
6.3.2Dijkstra算法
sendIndirectMessage方法的实现基于Dijkstra算法。
-
对于person1和person2,首先查询是否在同一个并查集的set中(即判断是否存在一条路径)
-
得到set的size,即循环的次数(每次循环确定一个点)
-
用Dijkstra算法得到person1到该set中任意一点的最短距离,用hashmap存起来
-
以后的每次查询,首先判断在hashmap中是否已经有结果,若有,直接返回,复杂度为O(1),若没有,则重复上述步骤,复杂度O(nlogn)
7. 心得和体会
本单元作业,主要通过JML规格训练培养我们阅读,完成JML规格的能力。在理解的基础上,如何高效地实现方法规格,则需要一定的技巧,需要一定的算法积累。不过好在这里所需要的算法在数据结构课程里已经讲过,因此,算法的完成不是难点,难点在于算法的优化。例如第三次作业里的Dijkstra算法的堆优化。
另一方面,通过本单元训练,我深刻明白JML规格在多人协作开发任务中的高效性。假如说本单元作业是课程组与我共同开发一个社交系统,假如没有JML规格,那么要实现一个社交系统,大概率是按照类来分工,然而,一个类极大概率上需要调用另一个类的方法,(如Network类调用Person类,Group类方法等)由于不同的类由不同的人实现,实现方式千差万别,甚至连方法名都无法统一,更别提协作了。而有了JML规格,课程组做的是构建框架,我做的是实现接口。中间通过JML规格,我可以很清楚的知道框架的每一步实现,每一个方法的作用,每一个类的作用。
浙公网安备 33010602011771号