OO第三单元总结
JML理论基础
JML(java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。
一般而言,JML有两种主要的用法:
-
-
针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。
本次作业即为JML开展规格化设计。根据给出的规格,实现各种方法。
-
注释结构
JML 以 javadoc 注释的方式来表示规格,每行都以 @
起头。有两种注释方式,行注释和块注释。其中 行注释的表示方式为 //@annotation
,块注释的方式为 /* @ annotation @*/
-
JML表达式
原子表达式:
\result
表达式:表示一个非 void
类型的方法执行所获得的结果,即方法执行后的返回值。
\old(expr)
表达式::用来表示一个表达式 expr
在相应方法执行前的取值。
\not_assigned(x,y...)
表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
\not_modified(x,y,..)
表达式:与上面的 \not_assigned
表达式类似,该表达式限制括号中的 变量在方法执行期间的取值未发生变化。
\nonnullelements(container)
表达式:表示 container
对象中存储的对象不会有 null
\type(type)
表达式:返回类型 type
对应的类型 (Class)
\typeof(expr)
表达式::该表达式返回 expr
对应的准确类型
量化表达式:
\forall
表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约 束。
\exists
表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约 束。
\sum
表达式:返回给定范围内的表达式的和。
\product
表达式:返回给定范围内的表达式的连乘结果。
\max
表达式:返回给定范围内的表达式的最大值。
\min
表达式:返回给定范围内的表达式的最小值。
\num_of
表达式:返回指定变量中满足相应条件的取值个数。
操作符:
推理操作符:b_expr1==>b_expr2
,如果b_expr1
为真,则b_expr2
为真。
子类型关系操作符:E1<:E2
,如果类型E1是类型E2的子类型,则该表达式的结果为真。
变量引用操作符:\nothing
指示一个空集,\everything
指示一个全集。
-
方法规格
前置条件:
前置条件通过requires
子句表示;requires P
,要求调用者确保P为真
后置条件:
后置条件通过ensures
子句表示;ensures P
,方法实现者确保方法执行返回结果一定满足谓词P的要求,确保P为真。
副作用范围限定:
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
关键词assignable
表示可赋值的modifiable
表示可修改的。
关键词pure
,表示方法既不会对对象的状态进行任何改变,也不需要提供输入参数。
signals子句:
signals
子句的结构为signals(***Exception e)b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e。
-
类型规格
不变式invariant
不变式时要求在所有可见状态下都必须满足的特性,语法上定义invariant P
,即在方法执行的过程中P始终为真。
状态变化约束constraint
对象的状态在变化时往往满足一些约束,用constraint
来对前序可见状态和当前状态的关系进行约束。
架构设计
由于为迭代开发,现仅选取第十一次作业的架构进行分析。
实际上,给出的JML规格已经搭建好了架构,我们只需要考虑每个类内部的数据结构,以及方法实现的算法。
设计策略
在完成这三次作业,我的设计策略都是
-
首先通读全部的代码以及规格,结合方法名称,推断每个方法的作用,以及各个类之间的联系。
多个Person
构建了Network
,每个Person
为图Network
的一个结点,结点和结点之间建立联系Relation
,加入到Edge
中,同时多个Person
可以构成小组Group
,各个Person
之间可以发送消息Message
。
-
分析清楚了各个类之间的联系,我选择从简单到复杂,首先补全
Person
中的方法,再是Group
,最后是Network
,Message
作为独立的部分,最后实现。 -
实现各个类中的方法时,我先考虑存储的数据结构,几乎每个类中均有
contains
和get
方法,以及每个Person
、
Group
、Message
都有各自的id,因此选择用HashMap来存储,Person
中有一个acquaintance
映射到value
的map,NetWork
中有由id映射到Person
,由id映射到group
,由id映射到message
的map。用HashMap存储可以免去了每次查找的循环遍历,尤其是在之后一些复杂的方法也均要进行判断contains
后,性能提升明显。 -
简单的方法可以很容易实现,对于复杂的方法,我添加新的类或者新的方法,对其进行了拆分。
数据结构及算法设计
-
Group中对ageSum和valueSum进行了显示存储,当addPerson和deletePerson时,修改ageSum和valueSum,降低了
getValueSum()
、getAgeMean()
、getAgeVar()
方法的复杂度 -
采用了并查集的数据结构,相比于dfs,并查集的数据结构大大降低了
isCircle(),queryBlockSum()
的复杂度。 -
最短路径采用了堆优化的
dijkstra
算法,相比于普通的dijkstra
算法,使用到了java的数据结构PriorityQueue
进行了堆优化。堆优化的dijkstra
算法更加适用于稀疏图。
bug分析
因为做了充分的测试,所以三次作业都没有出现正确性和性能的大翻车现象。但是,还是因为性能挂了几个点。
在第九次作业中,第一次接触JML,对规格的理解不够深刻,认为规格限制了数据结构,我基本都采用了ArrayList
顺序存储,contains()、get()
方法均为循环遍历,第十次作业我并没有意识到这个问题,没有对其进行修改,让我收获了两个ctle。除此之外, getValueSum() ,getAgeMean() ,getAgeVar()
方法中的循环遍历也导致了一个ctle。好在这两个bug都比较好修复。总而言之,循环遍历则性能危。
心得体会
本单元的作业相较于前两个单元难度小很多,给出了规格,我们只需要实现对应的方法,不必像前两个单元一样,花费大量的时间在架构上。这也让我明白了在好的架构下写代码,可以事半功倍,凡事不要一上来就一顿哐哐乱干。
本次作业的难点在复杂度分析和算法设计。暴力循环一时爽,修bug环节惨惨惨。但实际上,它不需要你对算法和精通掌握,只需要有这样意识,毕竟互联网上的板子到处都是。所以,这个单元的核心内容也没有算很偏移,重点还是在JML的理解上。