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 规范。

数据结构

由于 HashMapHashSet 在查询上的时间复杂度远远低于 ArrayList 等非哈希表实现的数据结构,因此在需要进行查询任务时,优选 HashMap 或者 HashSet 的数据结构。

具体来说,

  • Person
    • acquanintance & value: 注意到由于两者是相互绑定的,且 acquaninstance 不会重复,因此将两者用 HashMap 表示
  • Network
    • people: 注意到 Personid 不会重复,因此也采用 HashMap 实现
  • Exception:
    • 对于各类 Exception,由于功能类似,因此定义一个 Count 类作为各个 Exception 的成员之一,用于在每次调用时将调用次数 +1

算法

  • 第一次作业中复杂度相对较高的方法是 isCirclequeryBlockSum 方法。
    • 对于 iscircle,我采用了 并查集 的方法,并用递归的方法实现 find ,从而对并查集进行路径压缩的优化,使得整个树的结构只有两级。
    • 对于 queryBlockSum,我在 UnionFind 类中定义了属性 circleNum,在每次更改并查集时对 circleNum 进行相应的增减操作,从而使得查询的复杂度降为 O(1)
  • 用空间换时间
    • Network 中定义属性 peopleSum,用来记录总人数,避免查询人数时的遍历操作

UML

参见 HW3-3 中的 UML

bug

第一次作业在强测和互测中均未发现任何 bug。

测试

恰逢五一假期,因此并没有进行测试 = =

HW3-2

题目要求与分析

本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。

第二次作业相对第一次作业增加了 GroupMessage 两个类,需要阅读的 JML 也更长,更考察细心能力

数据结构

  • Person
    • messages: 由于 JML 要求其保持顺序,因此采用 LinkedList 存储
  • Group
    • people: 由于涉及到查找等操作,采用 HashSet 作为存储方式
  • Message
  • Network
    • groups: 注意到 groupId 不会重复,因此采用 HashMap 作为存储方式
    • messages: 注意到 MessageId 不会重复,且需要不断进行增删操作,因此采用 HashMap 作为存储方式

算法

  • queryGroupAgeMean & queryGroupAgeVar:

    总体的优化思路都是用空间换时间

    • 由于人数在不停变化,因此在组中设置 sizeageTotal 两个属性,方便通过 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 未做优化,导致时间复杂度过高,CTLE
  • queryGroupValueSum 计算有误(可能在增删 Person 的时候出现重复或者缺漏的情况)
  • 没有注意到 addToGroup 中对于人数小于 1111 的限制
  • queryGroupAgeVar 未做优化,导致时间复杂度过高,CTLE

HW3-3

题目要求与分析

本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。

第三次作业增加了不同的 Message 类型

数据结构

  • Network
    • emojiIdList & emojiHeatList: 考虑到 emojiId 不会重复,且两者一一对应,因此用 HashMap 将其统一存储

算法

本次作业主要的难点在于 sendIndirectMessage ,需要查找最短路径。实现方法是 堆优化 Dijkstra。在普通 Dijkstra 的基础上采用 PriorityQueue 进行存储,从而优化时间复杂度。

为了实现该算法,建立了 Link 类,存储 Person 和距离原点的距离 distance

UML

image

bug

吸取了上一次作业的惨痛教训,本次作业在强测和互测中未被测出任何 bug。

测试

依然采用了阅读代码 + 手动构造数据的方式进行测试,重点针对 sendIndirectMessagedeleteColdEmoji 进行测试,但未发现同屋人的 bug。

心得体会

1% 的错误会带来 100% 的失败。

相比于前两次作业,这次作业的难度要小很多,但失分却更为容易。

这次作业让我了解到了契约式设计,也认识到大项目的工作流程。本次作业中的 JML 语言在实际生活中的用途并不十分广泛,其原因之一在于它的工具链还尚待完善,而另一个原因在于它的可读性相对较低,因此比较容易发生错误。因此,在现实中,更多的契约式设计是采用设计文档的方式实现的。设计文档相对 JML 而言可读性更高,但表述上却很难做到严谨。但无论是哪种实现方式,都需要注重细节。

除此之外,这次作业也让我了解到了单元测试。开始接触 JUnit 的时候觉得很迷惑,想不通为什么要自己给自己设计测试样例,研讨课上同学的分享使我豁然开朗,在工程中,写代码和测试的往往是不同的人,因此会出现诸如 JUnit 等白盒测试方案。

总的来说,不能因为看似简单的需求就大意而不去做测试,由于粗心而造成的后果在以后的工作和学习中可能不是简单的 bug 修复就能解决的。

posted @ 2021-05-30 20:07  Ericaaaaaaaa  阅读(198)  评论(0编辑  收藏  举报