前言

这是2021级BUAA 面向对象课程第三单元实验——基于规格的层次化设计的博客总结。

架构设计

本单元作业需要根据 JML 规格描述实现一个社交关系模拟系统,需要阅读JML,实现高效图算法以及异常处理。下面主要从各单元图算法实现进行分析。

首先阅读 JML 发现许多方法需要通过 id 得到 person ,容易想到用 HashMap 进行存储。

hw9 —— 查询连通性和三角形关系数

通过阅读 JML 发现在 Network 类的 isCircle 方法查询给定两点的连通性, queryBlockSum 方法查询连通块数量,这显然可以通过并查集实现。为了体现类的封装性,我在 DisjointSet 类内实现了并查集的相关操作。

三角形关系数是指三人互为熟人。如果暴力求解,时间复杂度为 O ( N 3 ) O(N^3) O(N3) ,显然会 TLE。实际上这可以通过加边时的动态维护实现:

  • 在每次 addRelation 时,枚举除了 id1id2 的所有人,若他们与二人分别有边,则关系数加1。

  • 我们还可以进一步优化上述 O(N) 的维护算法。我们通过 BitSet 维护每个 PersonAcquaintance ,那么如果要得到二人共同的熟人只需要与一下。另外,由于 Personid 仅保证在 int 范围内,需要给每个 Person 一个 number 属性进行离散化操作。

    public class MyPerson implements Person {private static int tot = 0; // 共有多少人private int number; // 本人是第几个private BitSet bitSet; // 记录关系public MyPerson(int id, String name, int age) {...this.number = tot++;this.bitSet = new BitSet();}public void addAcquaintance(Person person, int value) {...bitSet.set(((MyPerson)person).getNumber());}
    }
    public class MyNetwork implements Network {public void addRelation(int id1, int id2, int value) {BitSet bitSet = (BitSet) ((MyPerson) getPerson(id1)).getBitSet().clone();bitSet.and(((MyPerson) getPerson(id2)).getBitSet());tripSum += bitSet.cardinality();}
    }
    

hw10 —— 有删边的连通性维护和动态维护最大值

modifyRelation 中,当修改后的 v a l u e ≤ 0 value \leq 0 value≤0 ,两人就不再是熟人了。然而并查集并不支持删边操作。此时我认为时间复杂度合适并且实现简单的是 写时复制的并查集,当进行了删边操作时,不选择立即重建并查集,而是标记 removeFlag = true;在需要查询连通性时,检查 removeFlag ,若为 true ,则重建并查集。另外当 removeFlag = false 时,并查集失效,此时不需要在 addRelation 中维护并查集。

queryBestAcquaintance 方法中,需要查询该 Person 的所有 Acquaintancevalue 最大的一位。这通过在新增或修改 value 过程中动态维护。当该 personbestAcquaintancevalue 减少或 acquaintance 删除时,遍历一遍所有 Acquaintance 维护。至于 queryCoupleSum 方法查询双方都是对方 value 最大的 acquaintance 则直接 O ( N ) O(N) O(N) 遍历即可。

public class MyPerson implements Person {public void addValue(int id, int value) {values.put(id, values.get(id) + value);if (value < 0 && id == bestId) {bestId = 23947392;bestValue = 0;for (Map.Entry<Integer, Integer> item : values.entrySet()) {if (item.getValue() > bestValue || (item.getValue() ==bestValue && item.getKey() < bestId)) {bestId = item.getKey();bestValue = item.getValue();}}} else {if (values.get(id) > bestValue || (values.get(id)== bestValue && id < bestId)) {bestId = id;bestValue = values.get(id);}}}public void addAcquaintance(Person person, int value) {acquaintance.put(person.getId(), person);values.put(person.getId(), value);bitSet.set(((MyPerson)person).getNumber());//if (bestValue < value || (bestValue == value&& bestId > person.getId())) {bestId = person.getId();bestValue = value;}}public void removeAcquaintance(int id) {bitSet.set(((MyPerson)acquaintance.get(id)).getNumber(), false);//acquaintance.remove(id);values.remove(id);if (id == bestId) {bestId = 23947392;bestValue = 0;for (Map.Entry<Integer, Integer> item : values.entrySet()) {if (item.getValue() > bestValue || (item.getValue() ==bestValue && item.getKey() < bestId)) {bestId = item.getKey();bestValue = item.getValue();}}}}public int getBestAcquaintance() {return bestId;}
}

hw11 —— 查询包含某点的最小环

在本次作业中 message 类型更多样,这些操作照着 JML 实现,一般不会有问题。

值得关注的是 queryLeastMoments 方法,查询包含某个点的最小环。首先想到的是 Floyed 算法,时间复杂度 O ( N 3 ) O(N^3) O(N3) ,必然 TLE。然后想到了删边的 Dijkstra 做法,时间复杂度有点危险(确实强测点会寄一个)。然后我学习了 Dijkstra+并查集 做法:

  1. 枚举要求经过的点 s;

  2. 用 Dijkstra 求单源最短路;

  3. 包含点s的最小环即为s到其余点的最短路的边加上一条不是最短路的边(该边有两种情况,见下图)。

实现细节可见代码:

 private int[] pre;private int[] dist;private void dijkstra(MyPerson root) {int peopleLength = root.getTot();pre = new int[peopleLength];dist = new int[peopleLength];boolean[] visited = new boolean[peopleLength];PriorityQueue<Pair<Integer, Integer>> queue =new PriorityQueue<>(Comparator.comparingInt(Pair::getKey));for (int i = 0; i < peopleLength; i++) {dist[i] = 999999999;pre[i] = i;visited[i] = false;}dist[root.getNumber()] = 0;Pair<Integer, Integer> pair = new Pair<>(0, root.getId());queue.add(pair);while (!queue.isEmpty()) {pair = queue.poll();MyPerson person = (MyPerson) getPerson(pair.getValue());if (visited[person.getNumber()]) { continue; }visited[person.getNumber()] = true;for (Integer item : person.getAcquaintance()) {MyPerson acquaintance = (MyPerson) getPerson(item);int value = person.queryValue(acquaintance);if (dist[acquaintance.getNumber()] > dist[person.getNumber()] + value) {if (!person.equals(root)) {pre[acquaintance.getNumber()] = person.getNumber();}dist[acquaintance.getNumber()] = dist[person.getNumber()] + value;queue.add(new Pair<>(dist[acquaintance.getNumber()], item));}}}}int find(int x) {if (pre[x] == x) { return x; }return pre[x] = find(pre[x]);}public int queryLeastMoments(int id) throws PersonIdNotFoundException, PathNotFoundException {if (!contains(id)) { throw new MyPersonIdNotFoundException(id); }// 方案:以起点能到达的点做单源最短路 + 并查集int result = 999999999;MyPerson person = (MyPerson) getPerson(id);dijkstra(person);for (Integer acquaintanceId : person.getAcquaintance()) {MyPerson acquaintance = (MyPerson) getPerson(acquaintanceId);if (pre[acquaintance.getNumber()] != acquaintance.getNumber()) {result = Math.min(result, person.queryValue(acquaintance) +dist[acquaintance.getNumber()]);}}for (Person p1 : people.values()) {MyPerson pp1 = (MyPerson) p1;if (pp1.equals(person)) { continue; }for (Integer item : pp1.getAcquaintance()) {MyPerson pp2 = (MyPerson) getPerson(item);if (!pp2.equals(person) && find(pp1.getNumber()) != find(pp2.getNumber())) {result = Math.min(result, dist[pp1.getNumber()] +dist[pp2.getNumber()] + pp1.queryValue(pp2));}}}if (result == 999999999) { throw new MyPathNotFoundException(id); }return result;}

测试

测试方法

白箱测试是指根据程序内部逻辑测试程序,检查程序中的每条通路是否按照预定要求正确工作(即穷举路径),要求测试者了解程序结构和处理过程。在程序写好后,根据程序内部逻辑手搓一些数据确保覆盖所有语句、分支、条件、路径,以及注意测试一些边界值和特殊情况。

黑箱测试 是指根据功能需求测试程序是否按照预期工作,基于系统的需求规格和功能规范来设计测试用例,并通过输入不同的数据和条件,观察系统的输出是否符合预期(即穷举输入),而不涉及程序的内部结构和内容特性。在写数据生成器时,按照功能需求构造数据,以发现系统是否满足所有功能要求以及是否存在错误或异常。

单元测试 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。它的目的是在开发过程中尽早发现代码中的缺陷,并确保每个功能模块都能够独立地正常运行。例如本单元中的 OK 测试方法,使用测试框架进行高度自动化的测试。

功能测试 是测试软件功能是否符合需求,通常采用黑箱测试方法。

集成测试 是软件测试的一种方法,用于验证不同组件、模块或子系统之间的集成是否正确、协同合作,并能够产生预期的结果。它旨在检测和解决在组件集成过程中可能出现的问题。在集成测试中,被测试的软件系统已经通过单元测试对各个组件进行了测试,并且这些组件已经通过了单元测试阶段。集成测试的目标是验证组件之间的接口、数据传递和交互是否正常,以及确保整个系统在集成后能够正确运行。

压力测试 是软件测试的一种方法,用于评估系统在超出正常工作负载条件下的性能和稳定性。它通过模拟系统面临的高负载、大数据量或高并发等情况,检查系统在这些极端情况下的表现和响应。压力测试的主要目标是找出系统的瓶颈、性能问题和资源耗尽情况,并评估系统在负载增加时是否能够满足性能要求。这种测试方法可以揭示系统的弱点、性能限制和潜在的故障,为系统的优化和调整提供指导。在本单元中,qlm,qbs 等指令时间复杂度较高,可以在数据构造中产生大量此类指令以观察时间性能。

回归测试 软件测试的一种方法,用于确认在进行软件修改、修复或增加新功能后,原有功能是否仍然正常工作,以及新的修改是否引入了新的错误或问题。当对软件进行修改时,无论是修复缺陷、添加新功能还是进行系统配置变更,都存在可能引入新的错误或导致原有功能出现问题的风险。回归测试的目的是在进行修改后,重新运行既有的测试用例,以验证软件系统在修改后的版本中是否仍然具有预期的行为。在本课程中,将迭代后的作业提交至上次作业的强测,观察原有功能是否仍然正常工作。

测试过程

由于本单元作业的实现并不复杂,白箱测试我通过人工逻辑分析与检查替代,在完成代码编写后,先浏览一遍,和 JML 比对,检查是否有疏忽,这能解决 60% 的 bug。

然后编写测试用例,主要通过生成随机性和大数据量的测试用例来完成黑箱测试,缺点是这样仍然难以保证覆盖性。

规格与实现分离

规格与实现分离是指将规格说明与具体实现分开,以实现更好的灵活性、可维护性和可扩展性。通过本单元作业,我认识到了规格与实现分离的一些好处:

  • 灵活性和可维护性。如果需要对系统进行功能增加、修改或优化,只需更改实现部分而不影响规格,从而降低了修改带来的风险,并提高了系统的灵活性和可维护性。通过阅读 JML 规格,可以很清晰地发现哪些实现需要修改,提高了效率。
  • 可测试性。根据规格编写测试用例,可以验证实现是否符合规格要求,从而提高软件的质量和可靠性。本单元作业同学们的架构比较相似,降低了互测阅读源码寻找bug的复杂性。

在 hw11 中,我在做 Dijkstra 使用 int dist[people.length] (得益于离散化操作,不需要使用 HashMap), 但是 run 类调用 addPerson 方法时,即使该 person 非法,我也赋予了他 myID ,导致 people.size != tot

在我的实现中,规格与实现分离的很好的一个体现就是在求连通性时新增了 DisjointSet 类实现并查集相关操作,也让后续维护更清晰。

OK测试方法

OK测试对于检验代码实现与规格的一致性的作用:

  • 通过对比代码实现与规格要求,可以发现代码中的错误和缺陷。
  • 确保代码实现了规格中定义的所有功能,并且没有遗漏或错误地实现了某些功能。

不过,我在本单元作业中对其感触不深,可能对我最大的用处就是在写OKTest时回忆检测的方法对象是否完全实现。因为理论上来说,OK测试应该调用我实现的方法判断数据处理前后是否满足规格,但显然这样课程组很难测试,于是课程组采用传入输入输出数据来判断,于是我的OKTest实现就变成了这样,我感觉和OK测试的初衷没啥关系:

public int deleteColdEmojiOKTest(int limit, ArrayList<HashMap<Integer, Integer>> beforeData,ArrayList<HashMap<Integer, Integer>> afterData, int result) {HashMap<Integer, Integer> beforeEmojis;beforeEmojis = beforeData.get(0);HashMap<Integer, Integer> beforeMessages;beforeMessages = beforeData.get(1);HashMap<Integer, Integer> afterEmojis;afterEmojis = afterData.get(0);HashMap<Integer, Integer> afterMessages;afterMessages = afterData.get(1);int num = 0;for (Map.Entry<Integer, Integer> item : beforeEmojis.entrySet()) {if (item.getValue() >= limit) {num++;if (!afterEmojis.containsKey(item.getKey())) { return 1; }}}for (Map.Entry<Integer, Integer> item : afterEmojis.entrySet()) {if (!beforeEmojis.containsKey(item.getKey())) { return 2; }if (beforeEmojis.get(item.getKey()) != item.getValue()) { return 2; }}if (afterEmojis.size() != num) { return 3; }num = 0;for (Map.Entry<Integer, Integer> item : beforeMessages.entrySet()) {if (item.getValue() != null) {if (afterEmojis.containsKey(item.getValue())) {num++;if (!afterMessages.containsKey(item.getKey())) { return 5; }else if (!Objects.equals(afterMessages.get(item.getKey()), item.getValue())) { return 5; }}} else {if (!afterMessages.containsKey(item.getKey())) { return 6; }else if (afterMessages.get(item.getKey()) != null) { return 6; }num++;}}if (afterMessages.size() != num) { return 7; }if (result != afterEmojis.size()) { return 8; }return 0;}

学习体会

本单元我觉得重点不在于代码的实现,而是基于规格的层次化设计,掌握了数据、方法、类的规格及其设计方法和基于规格的测试方法。编写 JML 规格可以提高设计的正确性和可迭代性,我认为这钟设计方式在未来大型项目团队合作中非常需要。

BUAA OO Unit3 JML规格相关推荐

  1. BUAA OO Unit3——JML

    文章目录 一.分析本单元测试过程 1.1 黑箱测试与白箱测试 1.1.1 黑箱测试 1.1.2 白箱测试 1.2 对单元测试.功能测试.集成测试.压力测试.回归测试的理解 1.2.1 单元测试 1.2 ...

  2. BUAA OO Unit3总结

    BUAA OO Unit3总结 写在前面 第三单元JML规格的学习告一段落,我认为这单元总体的内容并不困难,但如果想要获得理想的成绩,仍然需要刻苦地研究相关的单源最短路径算法,图论的知识也是必不可少的 ...

  3. JML规格编程——BUAA OO第三单元作业总结

    整体概述 这个单元整体围绕Java Model Language(JML)展开,通过学习JML规格了解契约式编程的过程,课上实验中进行了JML规格的简要编写,课下实验主要通过阅读规格并按照规格的要求正 ...

  4. [OO] Unit3 Summary JML系列

    文章目录 [OO] Unit3 Summary JML系列 JML理论基础 优点 语法基础 常用表达式 方法规格 类型规格 JML工具链 测试工具使用报告 SMT Solver EvoSuite JM ...

  5. 「BUAA OO Unit 3 HW12」第三单元总结 —— JML规格化设计与基于社交网络背景的图论算法

    「BUAA OO Unit 3 HW12」第三单元总结 目录 Part0 前言 Part1 测试分析 1.1 黑箱白箱 1.2 多种测试思路分析 1.2.1 单元测试 1.2.2 功能测试 1.2.3 ...

  6. OO第三单元JML规格总结分析

    OO 第三单元博客作业 1. JML语言概述 1.1 理论基础 综述 Java建模语言(Java Modeling Language,JML)是一种进行详细设计的符号语言,使人们用一种全新的方式来看待 ...

  7. OO第三单元总结——JML规格

    一.JML简介 1.JML语言的理论基础 JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言.JML是一种行为接口规格语言 (Behavior In ...

  8. BUAA OO 2019 第三单元作业总结

    目录 总 JML规格化设计 理论基础 工具链 规格验证 验证代码 代码静态检查 自动生成测试样例 生成结果 错误分析 作业设计 第九次作业 架构 代码实现 第十次作业 架构 代码实现 第十一次作业 架 ...

  9. BUAA OO 第三单元 规格化设计

    黑箱测试 黑箱测试将被测软件看做一个打不开的黑盒,主要根据功能需求设计测试用例进行测试. 可以检测软件功能能否按照需求规格说明书的规定正常工作,是否有功能遗漏,行为.性能是否满足要求等. (个人感觉像 ...

最新文章

  1. 删除计算机系学生的选课记录6,天津理工大学+数据库实验二.doc
  2. 【每周CV论文推荐】 初学深度学习单张图像三维人脸重建需要读的文章
  3. nova 命令汇总三 ——网络相关命令
  4. QT实现单遍线框渲染
  5. warning:This application is modifying the autolayout engine from a background thread
  6. 云小课 | 华为云KYON之VPC终端节点
  7. panel,dialog,window组件越界问题汇总
  8. JAVA锁之可重入锁和递归锁及示例代码
  9. java runnable线程锁_多线程 java 同步 、锁 、 synchronized 、 Thread 、 Runnable
  10. CentOS 7.6安装ZABBIX 4.4.0 + TimescaleDB
  11. Ubuntu c++调用python脚本(Anaconda)
  12. mysql将数据拟合曲线_有关任意多条曲线的拟合度算法
  13. python3.6实现的A星算法
  14. 红帽子linux 9.0下载,红帽子 RedHat linux 9.0 简体中文正式版 下载地址
  15. centos系统下安装daemontools详细指南
  16. x,y直角坐标系转经纬度WGS-84坐标系
  17. 我是一只IT小小鸟——读后感
  18. 【原创】新韭菜日记31--一个韭菜的自我修养,学习什么是白马蓝筹
  19. 五边形顶点坐标_足球顶点坐标的计算
  20. java nio rewind_java.nio.ByteBuffer中的flip()、rewind()、compact()等方法的使用和区别

热门文章

  1. BUCK电路控制方式
  2. 用虚幻4C++使用结构体和枚举
  3. 基于Servlet体系的HTTP请求代理转发Spring Boot组件
  4. 创建虚拟目录真实位置在/usr/local/maomao,内容自定,但允许abc和xyz用户通过密码访问
  5. 全志R329如何设置蓝牙自动重连时间或关闭自动重连?
  6. AM/Tribon自动套料接口简介
  7. 通过指针变量访问整型变量并打印变量地址
  8. 前端入门学习笔记六十五
  9. ThinkPHP图片上传的使用
  10. Visual Studio 2019中查看变量的内存地址