BUAA OO Unit 3
HW 3
本次作业主要是针对 JML
(Java Modeling Language) 进行相应的练习,结合一部分图论和算法的知识,根据 JML
描述构建社交网络。
作业的目标是学习并练习 基于规格的层次化设计,包括:
- 理解规格的概念
- 掌握方法的规格及其设计方法
- 掌握类的规格及其设计方法
- 掌握抽象层次下类规格之间的关系
- 掌握基于规格的测试方法
关于 JML
The Java Modeling Language (JML) is a behavioral interface specification language that can be used to specify the behavior of Java modules. It combines the design by contract approach of Eiffel and the model-based specification approach of the Larch family of interface specification languages, with some elements of the refinement calculus. -- [JML Homepage](The Java Modeling Language (JML) Home Page (ucf.edu))
JML 是一种面向 JAVA 的行为接口规格语言。用于实现基于规格的层次化设计和契约式设计(design by contrast)。
注释
//@ 行注释
/*@ 块
@ 注
@ 释
@*/
方法规格
/*@ public normal_behavior // 正常情况
@ requires // 前置条件(precondition)——执行前对输入的要求
@ modifies // 副作用(side-effects)范围限定——执行过程中对于环境(参数、所在 this)的改变描述
@ assignable // 副作用(side-effects)范围限定
@ effects // 后置条件(postcondition)——定义了过程所在所有未被 requires 排除的输入下给出的执行效果
@ ensures // 后置条件(postcondition)——执行后返回结果应该满足的约束
@ also
@ public exceptional_behavior // 异常情况
@ signals (SomeException e) // 抛出异常
@*/
public /*@pure@*/ ReturnType funcName(Object var1, ...) throws SomeException; // 函数名
类规格
/*@ public instance model non_null Type memberName;
@ invariant // 不变式——数据状态应该满足的要求
@ constraint // 修改约束——数据状态变化应该满足的要求
@ */
原子表达式
\result
:表示方法执行后的返回值old(exp)
: 表示一个表达式在方法执行前的取值not_assigned(var1, ...)
:表示括号中的变量不会在方法执行过程中被赋值not_modified(var1, ...)
:表示括号中的变量在方法执行过程中取值未发生变化nonnullelements(container)
:表示container
中存储的对象不会有null
typeof(type)
:返回元素类型
量化表达式
\forall
:表示给定范围内的元素,每个元素都满足相应的约束\exists
:表示给定范围内的元素,存在某个元素满足相应的约束\sum
:返回给定范围内的表达式的和\product
:返回给定范围内的表达式的连乘结果\max
:返回给定范围内的表达式的最大值\min
:返回给定范围内的表达式的最小值\num_of
:返回指定变量中满足相应条件的取值个数
操作符
<
:子类型关系操作符<==>
:等价关系操作符==>
:推理操作符\nothing
:变量应用\everything
:变量应用
JUnit 自动测试
Junit 是一个单元测试包,可以通过编写单元测试类和方法,来实现对类和方法实现正确性的快速检查和测试。还可以查看测试覆盖率以及具体覆盖范围(精确到语句级别),以帮助编程者全面无死角的进行程序功能测试。——指导书
教程:
断言测试
JUnit 包下的 Assert 提供了多个断言方法,用于测试后置条件或不变式是否满足 JML 规格要求。
assertEquals
:两个参数相等assertNotEquals
:两个参数不等assertSame
:若重写了equals
方法,则调用equals
,否则比较两个参数的内存地址assertNotSame
:若重写了equals
方法,则调用equals
,否则比较两个参数的内存地址assertTrue
:断言真assertFalse
:断言假assertNull
:断言空assertNotNull
:断言非空- ...
异常测试
@Test(expected = SomeException)
public void funcTest() {
}
@Test
public void funcTest() {
try {
} catch (SomeException) {
}
}
超时测试
@Test (timeout = 200)
public void funcTest() {
}
HW3-1
题目要求与分析
本次作业,需要完成的任务为实现
Person
类和简单社交关系的模拟和查询,学习目标为 JML 规格入门级的理解和代码实现。
第一次作业主要是让我们初步学习如何阅读和使用 JML 语言以及 JUnit 的配置方法。
实现的要求主要是指导书上面的少量文字描述和官方包中给出的 JML 规范。
数据结构
由于 HashMap
和 HashSet
在查询上的时间复杂度远远低于 ArrayList
等非哈希表实现的数据结构,因此在需要进行查询任务时,优选 HashMap
或者 HashSet
的数据结构。
具体来说,
Person
acquanintance
&value
: 注意到由于两者是相互绑定的,且acquaninstance
不会重复,因此将两者用HashMap
表示
Network
people
: 注意到Person
的id
不会重复,因此也采用HashMap
实现
Exception
:- 对于各类
Exception
,由于功能类似,因此定义一个Count
类作为各个Exception
的成员之一,用于在每次调用时将调用次数 +1
- 对于各类
算法
- 第一次作业中复杂度相对较高的方法是
isCircle
和queryBlockSum
方法。- 对于
iscircle
,我采用了 并查集 的方法,并用递归的方法实现find
,从而对并查集进行路径压缩的优化,使得整个树的结构只有两级。 - 对于
queryBlockSum
,我在UnionFind
类中定义了属性circleNum
,在每次更改并查集时对circleNum
进行相应的增减操作,从而使得查询的复杂度降为 O(1)
- 对于
- 用空间换时间
- 在
Network
中定义属性peopleSum
,用来记录总人数,避免查询人数时的遍历操作
- 在
UML
参见 HW3-3 中的 UML
bug
第一次作业在强测和互测中均未发现任何 bug。
测试
恰逢五一假期,因此并没有进行测试 = =
HW3-2
题目要求与分析
本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。
第二次作业相对第一次作业增加了 Group
和 Message
两个类,需要阅读的 JML 也更长,更考察细心能力
数据结构
Person
messages
: 由于 JML 要求其保持顺序,因此采用LinkedList
存储
Group
people
: 由于涉及到查找等操作,采用HashSet
作为存储方式
Message
Network
groups
: 注意到groupId
不会重复,因此采用HashMap
作为存储方式messages
: 注意到MessageId
不会重复,且需要不断进行增删操作,因此采用HashMap
作为存储方式
算法
-
queryGroupAgeMean
&queryGroupAgeVar
:总体的优化思路都是用空间换时间
- 由于人数在不停变化,因此在组中设置
size
和ageTotal
两个属性,方便通过ageMean = ageTotal / size
计算出组内人年龄的均值 - 在方差方面,这次为了优化时间,采用了公式 Var(x) = E(x^2) - E(x)^2。由于是概统中的公式,因此我自作聪明的认为会在保证正确性的基础上大大提高效率,没想到却翻车了。由于 JML 语言中方差的求法中有取证这一操作,因此利用上面的公式计算可能会精度过高,从而导致与正确结果之间存在差距。这点在强测中也可以体现,可见认真研读 JML 的重要性。
- 由于人数在不停变化,因此在组中设置
-
queryGroupValueSum
同样的,用空间换时间,在
Group
中添加属性valueSum
,并在增加和删除Person
以及Person
addRelation
时对其做出变化。需要注意的是 value 是否需要 ×2
UML
参见 HW3-3 中的 UML
bug
我可能不是在写代码,是在写 bug T_T
由于前面提到的自作聪明的方差算法和没有注意到 1111 的问题,在强测中获得了历史新低 30/100,能进互测屋就是个奇迹 = =
由于 bug 都很智障,因此在修复中也用不到五行代码就改完了,但所造成的错误是无法完全弥补的,而这些都可以通过仔细阅读 JML,仔细检查去避免。
测试
采用了阅读代码 + 手动构造边界数据的测试方法。重点放在 CTLE 和 1111 的检测上,共用 2 组数据 hack 出组内 10 个 bug,分别是
queryGroupValueSum
未做优化,导致时间复杂度过高,CTLEqueryGroupValueSum
计算有误(可能在增删Person
的时候出现重复或者缺漏的情况)- 没有注意到
addToGroup
中对于人数小于 1111 的限制 queryGroupAgeVar
未做优化,导致时间复杂度过高,CTLE
HW3-3
题目要求与分析
本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。
第三次作业增加了不同的 Message
类型
数据结构
Network
emojiIdList
&emojiHeatList
: 考虑到emojiId
不会重复,且两者一一对应,因此用HashMap
将其统一存储
算法
本次作业主要的难点在于 sendIndirectMessage
,需要查找最短路径。实现方法是 堆优化 Dijkstra。在普通 Dijkstra 的基础上采用 PriorityQueue
进行存储,从而优化时间复杂度。
为了实现该算法,建立了 Link
类,存储 Person
和距离原点的距离 distance
。
UML
bug
吸取了上一次作业的惨痛教训,本次作业在强测和互测中未被测出任何 bug。
测试
依然采用了阅读代码 + 手动构造数据的方式进行测试,重点针对 sendIndirectMessage
和 deleteColdEmoji
进行测试,但未发现同屋人的 bug。
心得体会
1% 的错误会带来 100% 的失败。
相比于前两次作业,这次作业的难度要小很多,但失分却更为容易。
这次作业让我了解到了契约式设计,也认识到大项目的工作流程。本次作业中的 JML 语言在实际生活中的用途并不十分广泛,其原因之一在于它的工具链还尚待完善,而另一个原因在于它的可读性相对较低,因此比较容易发生错误。因此,在现实中,更多的契约式设计是采用设计文档的方式实现的。设计文档相对 JML 而言可读性更高,但表述上却很难做到严谨。但无论是哪种实现方式,都需要注重细节。
除此之外,这次作业也让我了解到了单元测试。开始接触 JUnit 的时候觉得很迷惑,想不通为什么要自己给自己设计测试样例,研讨课上同学的分享使我豁然开朗,在工程中,写代码和测试的往往是不同的人,因此会出现诸如 JUnit 等白盒测试方案。
总的来说,不能因为看似简单的需求就大意而不去做测试,由于粗心而造成的后果在以后的工作和学习中可能不是简单的 bug 修复就能解决的。