zoukankan      html  css  js  c++  java
  • BUAA_OO第三单元总结性博客作业

    一、三次作业简单介绍

    第九次作业:

    实现两个容器类 Path 和 PathContainer。

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

    实现指令:

    添加路径 PATH_ADD 结点序列
    删除路径 PATH_REMOVE 结点序列
    根据路径id删除路径 PATH_REMOVE_BY_ID 路径id
    查询路径id PATH_GET_ID 结点序列
    根据路径id获得路径 PATH_GET_BY_ID 路径id
    获得容器内总路径数 PATH_COUNT
    根据路径id获得其大小 PATH_SIZE 路径id
    根据结点序列判断容器是否包含路径 CONTAINS_PATH 结点序列
    根据路径id判断容器是否包含路径 CONTAINS_PATH_ID 路径id
    容器内不同结点个数 DISTINCT_NODE_COUNT
    根据字典序比较两个路径的大小关系 COMPARE_PATHS 路径id 路径id
    路径中是否包含某个结点 PATH_CONTAINS_NODE 路径id 结点

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    第十次作业:

    实现容器类 Path 和数据结构类 Graph 。

    本次作业最终需要实现一个无向图系统。

    实现指令:

    容器中是否存在某个结点 CONTAINS_NODE 结点id
    容器中是否存在某一条边 CONTAINS_EDGE 起点结点id 终点结点id
    两个结点是否连通 IS_NODE_CONNECTED 起点结点id 终点结点id
    两个结点之间的最短路径 SHORTEST_PATH_LENGTH 起点结点id 终点结点id

     

     

     

     

     

    第十一次作业:

    实现容器类 Path 和地铁系统类 RailwaySystem。

    本次作业最终需要实现一个简单地铁系统。

    实现指令:

    整个图中的连通块数量 CONNECTED_BLOCK_COUNT
    两个结点之间的最低票价 LEAST_TICKET_PRICE 起点结点id 终点结点id
    两个结点之间的最少换乘次数 LEAST_TRANSFER_COUNT 起点结点id 终点结点id
    两个结点之间的最少不满意度 LEAST_UNPLEASANT_VALUE 起点结点id 终点结点id

     

     

     

    关键点 1 —— JML 规格:

    准确理解 JML 规格,然后使用 Java 来实现相应的接口,并保证代码实现严格符合对应的 JML 规格。

    关键点 2 —— 架构设计:

      本单元作业容器类的设计贯穿全程,且后一次作业需要使用前一次作业的容器类,同时还要继承前一次作业的容器类,需要仔细规划架构。

    关键点 3 —— Junit 单元测试:

      通过编写单元测试类和方法,来实现对类和方法实现正确性的快速检查和测试。还可以查看测试覆盖率以及具体覆盖范围(精确到语句级别),全面无死角的进行程序功能测试。

     

    二、JML 语言

    语言的理论基础

      JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言 (Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。

      一般而言,JML有两种主要的用法:

    1. 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
    2. 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

    注释结构:

      JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式 为 //@annotation ,块注释的方式为 /* @ annotation @*/ 。按照Javadoc习惯,JML注释一般放在被注释成分的近邻上部。

      1. requires子句定义该方法的前置条件(precondition), elements.length>=1 ,即IntHeap中管理着至少一个元 素;
      2. 副作用范围限定,assignable列出这个方法能够修改的类成员属性, othing是个关键词,表示这个方法不对 任何成员属性进行修改,所以是一个pure方法。
      3. ensures子句定义了后置条件,即largest方法的返回结果等于elements中存储的所有整数中的最大的那个 (max也是一个关键词)。

    JML表达式:

    原子表达式:

      1. esult表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。 esult表达式的类型就是方法声明中定义的返回值类型。
      2. old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。该表达式涉及到评估 expr 中的对象是否发生变化,遵从Java的引用规则,即针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。
      3. ot_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false 。实际上,该表达式主要用于后置条件的约束表示上,即限制一个方法的实现不能对列表中的变量进行赋值。
      4. ot_modified(x,y,...)表达式:与上面的 ot_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
      5. onnullelements( container )表达式:表示 container 对象中存储的对象不会有 null 。
      6. ype(type)表达式:返回类型type对应的类型(Class),如type( boolean )为Boolean.TYPE。TYPE是JML采用的缩略表示,等同于Java中的 java.lang.Class 。
      7. ypeof(expr)表达式:该表达式返回expr对应的准确类型。如 ypeof( false )为Boolean.TYPE。

    量化表达式:

      1. forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。 
      2. exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。 
      3. sum表达式:返回给定范围内的表达式的和。 
      4. product表达式:返回给定范围内的表达式的连乘结果。
      5. max表达式:返回给定范围内的表达式的最大值。
      6. min表达式:返回给定范围内的表达式的最小值。
      7. um_of表达式:返回指定变量中满足相应条件的取值个数。

    集合表达式:

      集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。

    操作符: 

      1. 子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果E1和E2是相同的类型,该表达式的结果也为真。
      2. 等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都是布尔表达 式,这两个表达式的意思是 b_expr1==b_expr2 或者 b_expr1!=b_expr2 。可以看出,这两个操作符和Java中的 == 和 != 具有相同的效果,按照JML语言定义, <==> 比 == 的优先级要低,同样 <=!=> 比 != 的优先级低。
      3. 推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1 。对于表达式 b_expr1==>b_expr2 而言,当 b_expr1==false ,或者 b_expr1==true 且 b_expr2==true 时,整个表达式的值为 true 。
      4. 变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来 引用相关的变量。 othing指示一个空集;everything指示一个全集,即包括当前作用域下能够访问到的所有变量。变量引用操作符经常在assignable句子中使用,如 assignable othing 表示当前作用域下每个变量都不可以在方法执行过程中被赋值。

    方法规格:

      • 前置条件(pre-condition):通过requires子句来表示: requires P;。其中requires是JML关键词,表达的意思是“要求调用者确保P为 真”。注意,方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。
      • 后置条件(post-condition) :通过ensures子句来表示: ensures P;。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执 返回结果一定满足谓词P的要求,即确保P为真”。同样,方法规格中可以有多个ensures子句,是并列关系,即方法实现者必须同时满足有所并列ensures子句的要求。
      • 副作用范围限定(side-effects) :副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词 assignable 或者 modifiable 。
      • signals子句 :signals子句的结构为 signals (Exception e) b_expr ,意思是当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e。

    应用工具链

      OpenJML:一款提供 JML 语言检查的开源编译器,可以检查 JML 语法、根据 JML 对代码实现进行静态检查。

      JML 编译器( jmlc ),是对 Java 编译器将带有 JML 规范注释的 Java 程序编译成 Java 字节码。编译的字节码包括检查的运行时断言检查指令。

      文件产生器( jmldoc ),生成包含 Javadoc 注释和任何 JML 规范的 HTML。 这便于将 JML 规范公布在网上。

      JMLUnitNG:一款根据 JML 自动构造样例的测试生成工具,用于进行单元化测试。由实践知生成的样例偏向于边界条件的测试。

    三、JMLUnitNG/JMLUnit

      首先按照讨论群伦佬的帖子测试简单的demo:

     1 // demo/Demo.java
     2 package demo;
     3 
     4 public class Demo {
     5     /*@ public normal_behaviour
     6       @ ensures 
    esult == lhs - rhs;
     7     */
     8     public static int compare(int lhs, int rhs) {
     9         return lhs - rhs;
    10     }
    11 
    12     public static void main(String[] args) {
    13         compare(114514,1919810);
    14     }
    15 }

      利用命令行操作:

      可以看到 JMLUnitNG 自动测试选择了很多在临界值,并且检查一些空的测试。

      根据尝试发现,JMLUnitNG 的确能够帮助我们提高程序的准确性,它选取很多容易出错的数据。

      然而,个人觉得 JMLUnitNG 自动测试针对代码和规格的要求还是非常严格的,这反而提高了测试的复杂度。

     

    四、三次作业的类图与度量分析

    度量分析标识:

      • ev(G)  本质复杂度

      • iv(G)   设计复杂度

      • v(G)   循环复杂度

      • OCavg  平均方法复杂度

      • OSavg  平均方法语句数(规模)

      • WMC  加权方法复杂度

      • v(G)avg 平均循环复杂度

      • v(G)tot 总循环复杂度

     

    第九次作业:

    1、架构构思:

      第一次作业主要就是通过继承 PathPathContainer 两个官方提供的接口,来实现自己的 PathPathContainer 容器。

      这部分按照官方给定接口的 JML 规格来实现即可。

      需要注意的是 Path 中采用两个HashMap:

    private HashMap<Integer, Integer> nodeList = new HashMap<>();
    private HashMap<Integer, Integer> distinctList = new HashMap<>();

      distinctList能帮助我们完成 containsNode()  PathContainer 的 getDistinctNodeCount()。

      另外要注意 equals() 可以先判断 hash 值是否相等,相等再进行比较。

      同样的,PathContainer 采用三个HashMap:

    private HashMap<Integer, MyPath> plist;
    private HashMap<MyPath, Integer> pidlist;
    private HashMap<Integer, Integer> distinctNode;

      plist 和 pidlist 双向存储,同时满足我们查找 ID 和 Path 的需求。

     

    2、项目分析:

    度量分析:

     

    3、自我总结:

      这次作业的复杂度问题主要就是 MyPath.compareTo()Mypath.equals(),这两个方法的 ev(G) 都在 Metrics 中出现了飘红。

      然而这个问题也是没有办法的,equals() 中我们可以先检查 hash 是否相等,如果相等再进行循环检查。

      而 compareTo() 则只能一股脑的检查下去,判断大小。

      所以循环是没有办法避免的。

      而 MyPathContainer.addPath() 的 v(G) 虽然没有飘红,但是复杂度最高的原因在于,我将 getDistinctNodeCount() 分摊到了每一次 add 和 remove

    1 for (Integer pathId : path) {
    2     if (distinctNode.containsKey(pathId)) {
    3         distinctNode.put(pathId, distinctNode.get(pathId) + 1);
    4     } else {
    5         distinctNode.put(pathId, 1);
    6     }
    7 }        

      其他方法的复杂度都得到了有效的控制。

     

    4、程序bug分析:

        这次作业比较简单,我的中测提交主要是不断简化程序,降低算法复杂度。我是严格按照 JML 规格编写的,所以并没有出现 bug

     

    5、性能分析:

      一方面我将 getDistinctNodeCount() 这一耗时利器分摊到了每一次 add 和 remove ,减少时间消耗。

      另一方面我从讨论区中张万聪大佬的帖子中受教,根据id和路径双向索引,使用 HashMap 双容器,一个是 HashMap<Integer, Path> ,一个是 HashMap<Path, Integer>,增删时同时考虑双方,查找就可以根据不同索引进行选择,保证了性能。

     

    6、互测分析:

      互测方面我主要检查了大家的 compareTo() 和 equals(),然后采用了大数据集测试程序的性能,虽然发现有写的比较复杂的,但本次作业的 CPU 时间给的很宽松,所以并没有查出 bug

     

    第十次作业:

    1、架构构思:

      本次作业的架构概况起来就是“大胆继承,小心重构”。

      因为 Graph 接口是继承了 PathContainer 接口的,所以我的 MyGraph 直接继承了上一次作业的 MyPathContainer 。

      从指令上来看,上一次作业主要是涉及点结构,而第二次作业的重点在边结构。在上一次作业中,我将 getDistinctNodeCount() 这一耗时利器分摊到了每一次 add 和 remove ,那么针对这一次作业,我需要重写 addPath()removePath()removePathById() ,继续将每一次的增加和删除操作分摊下去。

      同时设置 isModify 变量作为图结构的更新的标志。

      而针对新的指令扩展,因为“本次由路径构成的图结构,在任何时候,总节点个数(不同的节点个数,即 DISTINCT_NODE_COUNT 的结果),均不得超过250”,所以我采用静态数组,将节点离散化,把它们映射到[1,250]。

      针对最短路径,使用 Bfs 算法,采用 cache 机制,将每次计算目标结点之间距离时经过的中间结点的距离都保存下来,如果 isModify 被置 true,则清空 cache,重新计算。

     

    2、项目分析:

    UML类图:

    度量分析:

    3、自我总结:

      这次作业中的飘红主要是 graph.MyPath.compareTo()、graph.MyPath.equals()、graph.MyGraph.bfs() graph.MyGraph.cache(),其中 graph.MyPath.compareTo() graph.MyPath.equals() 在上次作业中已经分析过了,并没有办法降低 ev(G)。

      bfs() 属于标准算法,而 cache() 中牵扯到一个图的重构,这两个方法我在第十一次作业中进行了再一次重构,将复杂度降低了下去。在本次作业中的 cache() ,我是这样写的:

     1 for (int i = 0; i < super.getLength(); i++) {
     2         for (int j = 0; j < super.getLength(); j++) {
     3             if (i == j) {
     4                 renewGraph[i][j] = 0;
     5         } else if (graph[i][j] == 0) {
     6                 renewGraph[i][j] = inf;
     7         } else {
     8                 renewGraph[i][j] = 1;
     9         }
    10     }
    11 }        

    然而实际上只需要修改一下 bfs() ,在需要的时候判断一下,就能省去这个 O(n2的方法。当时可能是脑子轴了,没有想到。

     

    4、程序bug分析:

    这次作业我的强测爆的极其惨烈,原因在意“小心重构”的“小心”二字我没有做到。

    bug 出现在 MyPathContainer.removePath()  MyPathContainer.removePathById()。

    为了将图的更新均摊到每一次 remove 操作,最初我是这样写的:

     1 if (distinctNode.get(pathId) == 1) {
     2     distinctNode.remove(pathId);
     3     removeList.add(removeId);
     4     mapping.remove(pathId);
     5 
     6     for (int i = 0; i < length; i++) {
     7         if (graph[removeId][i] != 0) {
     8             graph[removeId][i] = 0;
     9             graph[i][removeId] = 0;
    10         }
    11     }
    12 } else {
    13     distinctNode.put(pathId, distinctNode.get(pathId) - 1);
    14         if (graph[removeId][prevId] != 0 && removeId != prevId) {
    15             graph[removeId][prevId] = 0;
    16             graph[prevId][removeId] = 0;
    17     }
    18 }

      每一次删除操作,我都将这个 Path 中的节点的所有边全部删除,如此错误的写法我当时居然没发现,实属不应该。

      因为这个头昏的写法,我的强测炸的妈都找不着了。

     

    5、性能分析:

      性能方面,MyGraph 继承了上一次作业的 MyPathContainer ,相关方法的复杂度得到有效控制。

      而本次作业相关的图操作,除了我上文提到的 cache() 的简化问题,bfs 最多跑 20×n 次,每次复杂度O(V + E),所以复杂度完全可以接受。

     

    6、互测分析:

      本次互测我采用的仍然是大数据集压力测试,检查同屋的复杂度问题和正确性问题,虽然我本地查出来了很多 bug,然而让我非常苦恼的是,我的数据一直交不上去,也不知道到底是哪里出问题了,非常难受,所幸助教大大在下一次互测中加入了互测样例错误提示。

     

    第十一次作业:

    1、架构构思:

      终于来到了本单元最后一次作业,我首先看了指导书的提示:

    本系列作业中

    • 每一次的功能是单调递增的,而且具备一定的继承性。
    • 所以建议大家采用继承的方式,将每一次的逻辑进行独立封装,以提高程序的工程性、可维护性。

    本次作业中

    • 请求数看似很多,实际上写指令很少,图结构变更指令更少。
    • 这毫无疑问意味着需要充分优化各类读指令。
    • 此外,由于图结构相对静态,所以可以考虑以下的策略
      • 将时间复杂度分散到本就无法降低复杂度的写指令以及线性复杂度指令中。
      • 将之前计算出来的部分中间结果保存下来,以减少后续的计算复杂度。
      • 一言以蔽之,应当使用类似缓存的思想,将大量的计算任务按照一定的策略进行均摊,以减少重复劳动。
    • 此外,由于本次涉及到大量图相关的计算,并且很多逻辑实际上很类似
      • 所以建议将图相关的计算进行单独的开类封装,并进行单独维护。
      • 同理,上文所说的缓存系统,也最好和图一样,进行单独维护(或者就以图的形态存在,不过还是建议尽可能降低耦合)。

      所以针对本次作业的 MyRailwaySystem, 采取的仍然是继承的思想,继承上一次作业的 MyGraph,同时重构 MyPathContainerMyGraph 中的部分方法,以适宜新的需求。

      另外,我构架了针对不同需求的 graphShortGraphTransGraphPriceGraphUnpleasantGraph,它们的架构都有很多相似的地方,所以都继承自同一个父类 RailGraph。虽然这样做显得有些臃肿和复杂,但我在设计时对我自身而言比较清楚,而且易于查找 bug

      对于图的构建,我主要采用了讨论区中拆点的方法,赋予不同功能的图不同的权值,以达到计算的需求。

    public static final int trans = 121;
    public static final int price = 2;
    public static final int Us = 32;

     

    2、项目分析:

    UML类图:

    度量分析:

     

    3、自我总结:

      这次作业中的飘红主要是 graph.RailGraph.dijkstra()、graph.RailGraph.getValue()、graph.ShortGraph.getBlockCount()、graph.ShortGraph.getLength()、railwaysystem.MyPath.compareTo() railwaysystem.MyPath.equals()。

      除去老生常谈的 compareTo()  equals(),其他的方法也可以进行一个分类:

      graph.ShortGraph.getLength() 是上次作业的 graph.MyGraph.bfs() 的一个修改,核心还是 bfs 算法。

      graph.ShortGraph.getBlockCount() 是采用并查集的思想,利用 bfs 算法进行连通块的计算。

      graph.RailGraph.getValue() 则是调用 graph.RailGraph.dijkstra() 进行满足各种需求的不同权值的带权路径的计算。

     

    4、程序bug分析:

      本次的强测依然炸的非常惨烈,bug 全都是 CTLE,所以我在性能分析中具体阐述。

     

    5、性能分析:

      在 bug 修复中我仔细分析了我的程序,发现了如下若干性能问题:

      对于 src/graph/ShortGraph.java ,我简化了 rebuild 方法,即将

     1 for (int i = 0; i < size; i++) {
     2     for (int j = 0; j < size; j++) {
     3         if (i == j) {
     4             graph[i][j] = 0;
     5         } else if (undigraph.getEdge(i, j) == 0) {
     6             graph[i][j] = inf;
     7         } else {
     8             graph[i][j] = 1;
     9         }
    10     }
    11 }

      舍弃,直接

    graph = undigraph.getGraph();

      简化程序,节约时间。同时,我将原来使用的 Floyd 算法更改为了 bfs 算法。

      对于 src/graph/RailGraph.java,我主要是更换了数据结构。

      即将 int[][] graph,更换为 ArrayList<arraylist<pair<integer, integer>>> edgeList。

      同时增加缓存机制 HashMap<integer, hashmap<integer,integer>> resultMap

      另外与 src/graph/ShortGraph.java 相似,我将我将原来使用的 Floyd 算法更改为了 dijkstra 算法。

    6、互测分析:

      又是一场腥风血雨。

      我的互测策略就是简易评测机对拍 + 自己构造超大数据集。

      同屋的 bug 还是很多的,重点集中在 LEAST_TRANSFER_COUNT 和 CTLE。

     

    五、心得体会

      规格能够避免自然语言的二义性,它更加严谨,功能描述更加确切。它既帮助自己实现要求的功能,同时帮助他人理解该段代码的作用。

      可以说,规格是为了团队协作、大型软件开发、严格功能定义项目所设计的。规格的重要性并不在于如何达到最好的设计效果,最好的性能要求,而是在于最正确的写法,最明确的功能定义。在开发时,可以用来确保不会因为不同的团队对于模块的功能和需求理解错误,导致开发过程出现问题。

      在实际使用和撰写规格后,我在感受到规格的优势时,也发现了撰写规格的难度。规格的撰写实际上是比编写代码更加艰难的过程。一方面在于撰写者需要确切理解需求,覆盖所有情况;另一方面当功能非常复杂时,规格也会非常复杂,这使得规格反而难以理解,这时候反而应该以自然语言为主,规格为辅

      长远看来,规格化编程是一项非常重要的能力,它在现今程序越来越复杂、参与人员越来越多、各种开源项目层出不穷的时代,它的需求会越来越大,所以我们必须提高自己对规格的理解程度。

     

  • 相关阅读:
    gmap 整理
    记录一次mybatis genertor使用以及mapper扫描遇见的问题
    mysql 记录
    NOIP2018Day1!!!题目出炉!!!
    图论——倍增求LCA
    干货系列——模板 之 图论1
    数学专题1
    动态规划——背包问题1:01背包
    图论——最短路——Dijkstra算法
    数据结构——并查集
  • 原文地址:https://www.cnblogs.com/tuoniao/p/10906277.html
Copyright © 2011-2022 走看看