面向对象OO第三单元总结

第三单元OO总结博客

 

1 梳理JML语言的理论基础、应用工具链情况

由于篇幅原因,这里只梳理几个在本单元常用的

注释结构

  1. 行注释://@annotation

  2. 块注释:/* @ annotation @*/

    例如:纯粹查询方法/*@ pure @ */,即方法的执行不会有任何副作用

 

JML表达式

原子表达式

  • \result:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值

  • \old(expr): 用来表示一个表达式 expr 在相应方法执行前的取值。作为一般规则,任何情况下,都应该使用\old把关心的表达式取值整体括起来。

量化表达式

  • \forall:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

    作业中的例子:

    1 (\forall int i; 0 <= i && (i < npath.length - 1); containsEdge(npath[i], npath[i + 1])));

    保证isConnected 的前提之一是,这两个点之间有连续的边将他们相连(否则不通)

  • \exist:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束

    作业中的例子:

    1 requires (\exists Path path; path.isValid() && containsPath(path); path.containsNode(fromNodeId)) &&
    2          (\exists Path path; path.isValid() && containsPath(path); path.containsNode(toNodeId));

    保证isConnected 的前提之一,要存在某个Path,这个Path上包含这个点(否则就Not Found Exception)

     

操作符

  • 子类型关系操作符: E1<:E2

  • 等价关系操作符: b_expr1<==>b_expr2或者 b_expr1<=!=>b_expr2

  • 推理操作符: b_expr1==>b_expr2 或者b_expr2<==b_expr1。对于表达式b_expr1==>b_expr2 而言,当 b_expr1==false ,或者b_expr1==trueb_expr2==true时,整个表达式的值为 true 。

  • 变量引用操作符

    • \nothing指示一个空集:

      例如:assignable \nothing表示当前作用域下每个变量都不可以在方法执行过程中被赋值

    • \everything指示一个全集

方法规格

正常行为规格normal_behavior

前置条件
  • 通过requires子句来表示

    • 例如增加一条路径的前提条件:增加的这条路径必须合法有效

      1 requires path != null && path.isValid();
后置条件
  • 通过ensures子句来表示

    • getPathId必须确保返回的ID确实属于当前存在的路径

    • 1 ensures (\exists int i; 0 <= i && i < pList.length; pList[i].equals(path) && pidList[i] == \result);
副作用
  • 使用关键词 assignable 或者 modifiable,这个函数要改什么变量,就在后面加那个变量的名字

    • 例如addPath

       1  /*@ normal_behavior
       2       @ requires path != null && path.isValid();
       3       @ assignable pList, pidList; 需要修改这两个List
       4       省略一部分
       5       @ also
       6         省略一部分
       7       @ assignable \nothing; 如果Path不合法,那么返回0,没有需要修改的东西
       8       @ ensures \result == 0;
       9       @*/
      10     public int addPath(Path path);

       

       

异常行为规格expcetional_behavior

  • also: 有两种场景使用

    1. 除了正常功能规格外,还有一个异常功能规格,需要使用also来分隔。

    2. 父类中对相应方法定义了规格,子类重写了该方法,需要补充规格,这时应该在补充的规格之前使用also;

     

  • signals子句

    • 结构为signals (***Exception e) b_expr,意思是当b_expr为true时,方法会抛出括号中给出 的相应异常e。

      例子:

       1 /*@ public normal_behavior
       2      省略
       3       @ also 下面是异常行为
       4       @ public exceptional_behavior 
       5       @ assignable \nothing;
       6       @ signals (PathNotFoundException e) path == null;
       7       @ signals (PathNotFoundException e) path.isValid() == false;
       8       @ signals (PathNotFoundException e) !containsPath(path);
       9       @*/
      10     public int removePath(Path path) throws PathNotFoundException;
      • 当 path == null条件成立,抛出异常

      • 当 path.isValid条件成立,抛出异常

      • 当容器内不含 path,抛出异常

 

在线OpenJML链接

 

2 部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果

跳过

 

3 部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例, 并结合规格对生成的测试用例和数据进行简要分析

生成数据

我研究了好久貌似OpenJML对MacOS不是很友好……好像也没有搞清楚TestNG怎么生成测试样例

写了一个python程序来生成测试指令

 1 import random
 2 SEQUENCE = 0;
 3 ID = 1;
 4 DOUBLE_ID = 2;
 5 NONE = 3;
 6 ID_NODE = 4;
 7 queries = [("PATH_ADD", SEQUENCE),
 8            ("PATH_REMOVE", SEQUENCE),
 9            ("PATH_REMOVE_BY_ID", ID),
10            ("PATH_GET_ID", SEQUENCE),
11            ("PATH_GET_BY_ID", ID),
12            ("PATH_COUNT", NONE),
13            ("PATH_SIZE", ID),
14            ("PATH_DISTINCT_NODE_COUNT", ID),
15            ("CONTAINS_PATH", SEQUENCE),
16            ("CONTAINS_PATH_ID", ID),
17            ("DISTINCT_NODE_COUNT", NONE),
18            ("COMPARE_PATHS", DOUBLE_ID),
19            ("PATH_CONTAINS_NODE", ID_NODE)
20            ]
21 
22 NUMBER = 100
23 PATH_LENGTH = 500
24 query_count = len(queries)
25 path_count = 0
26 
27 
28 def gen_node():
29     """generate random node"""
30     return random.randint(-2**31, 2**31 -1)
31 
32 def gen_none():
33     """"""
34     return []
35 
36 def gen_sequence():
37     """generate id sequence"""
38     len = random.randint(2, PATH_LENGTH)
39     sequence = []
40 
41     for i in range(len):
42         sequence.append(gen_node())
43 
44     path_count + 1;
45     return sequence
46 
47 def gen_id():
48     """generate id"""
49     return [random.randint(1, path_count * 2 + 1)]
50 
51 def gen_doubel_id():
52     """generate two ids"""
53     return [random.randint(1, path_count + 1), random.randint(1, path_count + 1)]
54 
55 def gen_id_node():
56     return [random.randint(1, path_count + 1), gen_node()]
57     
58 # print(gen_sequence())
59 
60 generators = {SEQUENCE : gen_sequence,
61               ID: gen_id,
62               DOUBLE_ID: gen_doubel_id,
63               NONE: gen_none,
64               ID_NODE: gen_id_node }
65 
66 for i in range(13):
67     (op, arg) = queries[0]
68     print(op + ' ' + ' '.join(map(lambda x: str(x), generators[arg]())))
69 for i in range(NUMBER):
70     (op, arg) = queries[random.randint(0, query_count - 1)]
71     print(op + ' ' + ' '.join(map(lambda x: str(x), generators[arg]())))

 

IDEA TestMe插件和 TestNG和OpenJML的安装历程

  1. 安装TestMe插件,可以直接在Plugin 的Market里面下载,TestNG失败

  2. 下载TestNG

(TestNG下载及安装教程链接)

(TestNG6.8.7JAR包的下载链接)

Build TestNG from source code

TestNG is also hosted on GitHub, where you can download the source and build the distribution yourself:

 

然后把Jar包导入新创建的Maven项目当中,课程组的Jar包导入项目当中

在右侧边栏的Maven,点击加号,把课程组给的pom.xml导入

后来弄出了形如这样的一堆令人头疼的代码,遂放弃

 
 1     @Test
 2     public void testGetConnectedBlockCount() {
 3         when(bfs.getBlockSize()).thenReturn(0);
 4         when(bfs.updateGraph(any())).thenReturn(new HashMap<Integer, HashMap<Integer, Integer>>() {{
 5             put(Integer.valueOf(0), new HashMap<Integer, Integer>() {{
 6                 put(Integer.valueOf(0), Integer.valueOf(0));
 7             }});
 8         }});
 9 
10         int result = myRailwaySystem.getConnectedBlockCount();
11         Assert.assertEquals(result, 0);
12     }

(错误示范)

 

正确使用OpenJML

在许多大佬的帖子中已经把如何生成测试代码讲得很清楚了

示例主程序在Demo.java里面

java -jar jmlunitng.jar src/Demo.java 
javac -cp jmlunitng.jar src/*.java
 

然后会出现很多名字奇怪的.java文件,其中有一个叫Demo_JML_Test.java,也就是测试时要运行的主程序

我把生成的.java测试文件都加入到IDEA工程的src文件夹下面,需要导入的包有openjml.jar,jmlunitng.jar

但是我在运行Demo_JML_Test的时候有一些奇怪的报错 “org.jmlspecs.utils.JmlAssertionError 中是 protected访问控制”

于是把报错代码Catch 的 Exception删了

 

在艰苦卓绝的研究下,和修改了一堆版本不兼容的问题后,测试代码终于运行成功了!

(racEnabled尚未解决)

 

Junit 测试代码

创建单元测试可以直接在IDEA里面自动生成一个测试代码的框架

 

 

Junit中集中基本注解,是必须掌握的:
  • @BeforeClass – 表示在类中的任意public static void方法执行之前执行

  • @AfterClass – 表示在类中的任意public static void方法执行之后执行

  • @Before – 表示在任意使用@Test注解标注的public void方法执行之前执行

  • @After – 表示在任意使用@Test注解标注的public void方法执行之后执行

  • @Test – 使用该注解标注的public void方法会表示为一个测试方法

 

junit中的assert方法全部放在Assert类中,总结一下junit类中assert方法的分类。

Junit中部分Assert方法总结:(来自这个博客)
  1. assertTrue/False([String message,]boolean condition); 判断一个条件是true还是false。感觉这个最好用了,不用记下来那么多的方法名。

  1. fail([String message,]); 失败,可以有消息,也可以没有消息。

  1. assertEquals([String message,]Object expected,Object actual); 判断是否相等,可以指定输出错误信息。 第一个参数是期望值,第二个参数是实际的值。 这个方法对各个变量有多种实现。在JDK1.5中基本一样。 但是需要主意的是float和double最后面多一个delta的值。

  1. assertNotEquals([String message,]Object expected,Object actual); 判断是否不相等。 第一个参数是期望值,第二个参数是实际的值。

  1. assertArrayEquals([java.lang.String message,] java.lang.Object[] expecteds, java.lang.Object[] actuals) ;

  1. assertNotNull/Null([String message,]Object obj); 判读一个对象是否非空(非空)。

  1. assertSame/NotSame([String message,]Object expected,Object actual); 判断两个对象是否指向同一个对象。看内存地址。

  1. failNotSame/failNotEquals(String message, Object expected, Object actual) 当不指向同一个内存地址或者不相等的时候,输出错误信息。 注意信息是必须的,而且这个输出是格式化过的。

下面仅写出部分函数的测试代码

 1 public class MyGraphTest {
 2     MyGraph graph = new MyGraph();
 3     int[] nodelist1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13};
 4     int[] nodelist2 = {-1, 6, -2, -3, -4, -5, -6, -7, -8};
 5     int[] nodelist3 = {0,1205, 999, 888, 777, 666};
 6     @Before
 7     public void before() throws Exception { // 每个方法检测之前,都另这个图里面有这三条路径
 8         System.out.println("Ready to test " );
 9         //MyGraph graph = new MyGraph();
10         graph.addPath(new MyPath(nodelist1));
11         graph.addPath(new MyPath(nodelist2));
12         graph.addPath(new MyPath(nodelist3));
13     }
14 
15     @After
16     public void after() throws Exception { // 每个函数检测完毕都会输出“test successfully!”
17         System.out.println("test successfully!");
18     }
19 
20 @Test
21 public void testContainsPath() throws Exception { 
22 //TODO: Test goes here...
23     System.out.println("Testing ContainsPath!");
24     Assert.assertTrue(graph.containsPath(new MyPath(nodelist1)));
25     Assert.assertTrue(graph.containsPath(new MyPath(nodelist2)));
26     Assert.assertTrue(graph.containsPath(new MyPath(nodelist3)));
27     int[] nodelist4 = {9,9,9,9};
28     Assert.assertFalse(graph.containsPath(new MyPath(nodelist4)));
29 } 
30 
31 @Test
32 public void testContainsPathId() throws Exception { 
33     System.out.println("Testing contains Path id!");
34     Assert.assertTrue(graph.containsPathId(1));
35     Assert.assertTrue(graph.containsPathId(2));
36     Assert.assertTrue(graph.containsPathId(3));
37     Assert.assertFalse(graph.containsPathId(100));
38 } 
39 
40 
41 @Test
42 public void testRemovePath() throws Exception { 
43 //TODO: Test goes here...
44     graph.removePath(new MyPath(nodelist1));
45     Assert.assertFalse(graph.containsPathId(1));
46 } 
47 
48 @Test
49 public void testRemovePathById() throws Exception { 
50 //TODO: Test goes here...
51     graph.removePathById(2);
52     Assert.assertFalse(graph.containsPath(new MyPath(nodelist2)));
53 } 
54 
55 @Test
56 public void testContainsNode() throws Exception { 
57 //TODO: Test goes here...
58     Assert.assertTrue(graph.containsNode(999));
59     Assert.assertFalse(graph.containsNode(14));
60 
61 
62 @Test
63 public void testIsConnected() throws Exception { 
64 //TODO: Test goes here...
65     System.out.println("Testing is Connected@");
66     Assert.assertTrue(graph.isConnected(2,-8)); // 判断这个连接是对的
67     Assert.assertTrue(graph.isConnected(13,-8));
68     Assert.assertFalse(graph.isConnected(0, 1));
69     // 把这个文件拖到src文件夹里面即可运行
70 
71 }
72 
73 }

愉快的Tests passed! 😄

 

4 按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构

第一次作业

第一次作业比较简洁,接口已经提供好了(超级良心),实现函数的需求即可。

需要重视的几个需求:

根据ID获得路径&根据路径获得ID:

  • 构造两个HashMap,key 和 value 反向存储一次,一个是<id, path>,一个是<path, id>,这个构造可以解决大多数基本需求

  • 注意自己写的类,需要重写equals()方法和 hashCode()方法,不重写的话,每次判断路径都是不一样的

    Object类默认的equals比较规则就是比较两个对象的内存地址。而hashCode是本地方法,java的内存是安全的,因此无法根据散列码得到对象的内存地址,但实际上,hashcode是根据对象的内存地址经哈希算法得来的。

  •  
     1 private HashMap<Integer, Path> container; // 用来装路径的<ID, path>
     2 private HashMap<Path, Integer> reverseContainer;
     3 // 下面是重写的方法
     4 @Override
     5     public boolean equals(Object obj) {
     6         if (path == null || !(obj instanceof Path)) {
     7             return false;
     8         }
     9         // obj instanceof Path
    10         Path path = (Path) obj;
    11         if (this.size != path.size()) {
    12             return false;
    13         }
    14         int min = Math.min(this.size, path.size());
    15         //Iterator iter = this.iterator();
    16         for (int i = 0; i < min; i++) { // 一个个比较节点,
    17             if (this.getNode(i) != path.getNode(i)) {
    18                 return false;
    19             }
    20         } // 如果所有节点都完全相同,那么这两个路径就是等价的
    21         return true;
    22     }
    23 
    24     @Override
    25     public int hashCode() {
    26         int result;
    27         // 至少两个节点,所以可以利用第二个,减少哈希冲突
    28         result = this.size() * this.getNode(0) * this.getNode(1);
    29         return result;
    30     }

     

     

容器内不同节点个数

  • 由于哈希表速度快,查找方便,键-值一一对应,故也选用哈希表

  • 1 private HashMap<Integer, Integer>  distinctCount; // <nodenum, 该点出现次数>

比较两个路径大小关系

  • 节点逐个比较即可,有个小坑,返回值要严格大于/小于/等于0,所以我就没有用x-y的形式

     1     @Override
     2     public int compareTo(Path o) {
     3         int min = Math.min(o.size(), this.size);
     4         for (int i = 0; i < min; i++) {
     5             if (this.getNode(i) == o.getNode(i)) {
     6                 continue;
     7             }
     8             else if (this.getNode(i) < o.getNode(i)) {
     9                 return -1;
    10             }
    11             else if (this.getNode(i) > o.getNode(i)) {
    12                 return 1;
    13             }
    14         }
    15         // 一路上都一样大
    16         if (this.size > o.size()) {
    17             return 1;
    18         } else if (this.size == o.size()) {   //if ()
    19             return 0;
    20         } else {
    21             return -1;
    22         }
    23     }

     

     

第二次作业

第二次作业在第一次的基础上,将PathContainer升级成为了Graph类,不再是简单的“路径存储管理器”,而变成了一个无权无向图。

通过分析新增需求,我对架构做出了如下调整:

容器中是否存在某个结点:

  • 直接把上次维护的distinctCount这个HashMap拿来用,containsKey即可

 

是否存在某一条边 & 结点是否连通 & 结点间最短路径

出于速度的考虑,我维护了两个HashMap套HashMap的结构

  • 一个是“种子”,仅仅记录<nodeid1, <nodeid2, 这两个点构成的边出现的次数>>,在增删时维护很方便

  • 一个是通过“种子”生成的“图”,<nodeid1, <nodeid2, 这两个点的最短距离>>,查询时直接返回

  • 注意nodeid2是与id1连接的所有点,一个nodeid1可对应很多邻接点nodeid2,也就是一个结点一张表

    1     // 维护Edge的第一个HashMap,一个结点对应直接连接的邻接点,增删时维护
    2     // 维护邻接点(一个边)的信息,
    3     private HashMap<Integer, HashMap<Integer, Integer>> edgeCount; // 种子
    4 
    5     // 维护Edge的第二个HashMap, 存储两个间接连接的Node之间最短路径长度, 查询时维护
    6     private HashMap<Integer, HashMap<Integer, Integer>> connectedMap; //

     

那么,如何由“种子”生成“图”呢?我考虑了不同方法找最短路径,最终选择了BFS广搜

新建一个Bfs类,里面只有两个函数:
1 public HashMap<Integer, Integer> PerformBfs(int startNode,
2        HashMap<Integer, HashMap<Integer, Integer>> edgeCountGraph) 
3     // 这个传进去的参数 edgeCountGraph 就是上面维护的“种子”
  • PerformBfs用于单次广搜,找出从startNode起点出发,所可以到达的所有点的最短路径

  • 由于BFS算法的特性,一旦到达某个点,那么所经过的路径一定是最短的,直接存到一个HashMap即可。

    PerformBfs返回的HashMap就是我第二个大“图”所用的内层HashMap

    存的键-值是 <startNode可达点ID,startNode到该点路径>

 
1 public HashMap<Integer, HashMap<Integer, Integer>>
2        updateGraph(HashMap<Integer, HashMap<Integer, Integer>> edgeCountGraph) 
  • updateGraph通过调用N次(N是容器内不同点个数)PerformBfs,遍历edgeCountGraph,把每个点到它所有可达点都算一遍,并存下最短路径值(其实也是双向存储),这样就做好了我们的大“图”

  • 外面的Graph类只需要把增删时维护好的“种子”放进来,直接调用updateGraph即可得到一张完整的“图”及其最短路径,拿出去查询就可以了,不需要关心是怎么实现的

  • 当然,我们并不需要每次查询指令来的时候都重新计算,设置一个布尔变量upToDate,每当addPath或者removePath,对“种子”进行操作的时候,upToDate = false。当查询指令到来时,如果upToDate == false,那么调用一下updateGraph,将uoToDate变量设为true;否则,直接查阅

所以,“是否存在某一条边”可通过“种子”edgeCount来解决。

而是否连通,只要能算出来最短路径的(connectedMap内外层Key同时含有两个结点的ID),一定连通

 

第三次作业

这次在图的基础上,升级成为了RailwaySystem类(吐血)

加入了四个新的魔鬼需求:

  • 连通块个数

  • 最少票价

  • 最少换乘

  • 最少不满意度

下面我们来一一分析:

连通块个数

突然发现,连通块在BFS的时候就已经维护好了!

connectMap外层Key对应的HashMap就包含了它所有的可达点,所以它和它的可达点们构成了一个连通块!

沿用上次的Bfs类,在类中新建一个ArrayList,里面存Set集合(结点无序,不重复,所以用Set)

1 private ArrayList<HashSet<Integer>> connectedBlock; // 需要随着最短路径updateGraph
2 // 一个Set就是一个连通块
3     public int getBlockSize() {
4         return connectedBlock.size();
5     }

遍历我们创建的大“图”,connectedMap外层结点,如果它已经存在于ArrayList中某个Set,直接跳过;

如果该结点尚未出现在任何Set里面,那么把它和它内层的所有可达点都放到一个Set,加入ArrayList

最终,返回这个ArrayList的大小,即为连通块个数

 

最少票价 & 最少换乘 & 最少不满意度

这三个问题在需求上是等价的,变成了无向带权图,我们可以列一个表格

 非换乘边权重换乘边权重
票价 1 2
换乘数 0 1
不满意度 U(E) (神秘公式) 32

这下不能愉快的BFS了,通过分析各路大佬提出来的思路,有拆点建站台和不拆点的方法,我写了两天拆点和Dijkstra,还用HashMap实现,心态爆炸,于是转向了优雅的Floyd和静态数组

思路
  • 将每一个Path内所有的结点都连接,变成直连点,构成path.size大小的完全无向带权图

  • 各个Path之间的共同点合并为1个点(也就是说integer型的ID就是结点唯一的标识)

  • 每一条路径找出每个点到该路径内所有点的最短路径,也就是完全图的边权

  • 这样,多走一条边就是多换乘1次,例如走了两条边就相当于从一个Path换乘到另一个Path,换乘一次,走N条边就是换乘了N-1次

  • 为了方便计算,把所有边权都写成如上表所示,每一条边的权重都是“非换乘边权重”,多走一条边,票价+2,换乘+1,不满意+32;

封装一个Floyd类,里面仅仅含有一个函数
public int[][] perfromFloyd(int[][] a, int totalNodes)
  • perfromFloyd传进去的参数是邻接矩阵a,不同点个数totalNodes

  • 返回的是最短路径矩阵(三层循环,莽就完事儿了)

  • 三个需求都可以用这个perfromFloyd算,区别就是构建邻接矩阵时边权不同

RailwaySystem 类新增三个邻接矩阵
private int[][] priceResult; // 票价
private int[][] transResult; // 换乘
private int[][] unpleasantResult; // 不满意度

public void FloydNewRequest() // 在增删路径时统一处理这三个需求,更新上面的数组
  • 使用Floyd先对每一条路径找出每个点到该路径所有点的最短路径

  • 再用Floyd对每一个点的可达点找最短路径

  • 每次增删都要遍历已有的pathContainer(HashMap<Path, Integer>)重新构建邻接矩阵

 

5 按照作业分析代码实现的bug和修复情况

在本单元的三次作业当中,强测均为100分,互测中没有被找出bug(组员都比较平和)

 

 

6 阐述对规格撰写和理解上的心得体会

  1. 因为初次接触规格,所以还需要花一定的时间来熟悉它。规格描述确实让我们少了很多理解上的歧义,也许在更大的工程当中使用起来可以提高效率。

  2. 第三题真的好难部署啊,呜呜呜

  1. 令我比较惊喜的是,在第三次作业中,不拆点的Floyd算法貌似表现得不错,没有炸TLE

  1. 好像规格在实际作业中很少看……第三次作业被搞成了算法作业

     

 

 

posted @ 2019-05-22 00:53  Vanellope  阅读(307)  评论(0编辑  收藏  举报