OO第三单元作业分析

第三次博客作业

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

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言,主要用于开展规格化设计和针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。

  1. JML语言的理论基础

(1)前置条件:requires子句定义该方法的前置条件,对参数进行限制。

(2)副作用范围限定:assignable列出这个方法能够修改的类成员属性。

(3)后置条件:ensures子句定义了后置条件,对方法的执行结果进行限制。

(4)其他重要语法:

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

\old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。

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

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

\max表达式:返回给定范围内的表达式的最大值。

\min表达式:返回给定范围内的表达式的最小值。

  1. 应用工具链情况

a. 使用 OpenJML对含有JML标记的代码进行不同检查:

(1)-check 选项可以对java文件进行JML规范检查,检查是否有语法错误,比如: openjml -check Demo.java

(2)-rac 选项可以对代码进行运行时的检查,比如:openjml -rac Demo.java

(3)-esc 选项能对程序代码进行静态检查,看是否有潜藏的问题

b.使用 JMLUnitNG根据JML语言自动生成TestNG测试,或是JMLUnit检查程序代码的正确性。

我个人感觉,它的好处是可以帮助你保证程序的正确性,但是JMLUnitNG和JMLUnit也有局限性,具体讨论见下板块。

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

我按讨论区说的方法,安装好了openjml和JMLUnitNG,其中遇到的不少阻力就不赘述了,这是我用demo.java的例子:

先编译,共分为两步:

  • 用 javac 编译 JMLUnitNG 的生成文件
  • 用 jmlc 编译自己的文件,生成带有运行时检查的 class 文件

javac -cp ../../../../openjml/jmlunitng.jar *.java

java -jar ../../../../openjml/openjml.jar -rac Demo.java

java -cp jmlunitng.jar Demo_JML_Test

之后生成了测试样例,如图:

img1

public class Demo {
    /*@ public normal_behaviour
      @ ensures \result == lhs - rhs;
    */
    public static int compare(int lhs, int rhs) {
        return lhs + rhs;
    }

    public static void main(String[] args) {
        compare(115514,1919810);
    }
}

[TestNG] Running:
Command line suite

Passed: racEnabled()
Passed: constructor Demo()
Failed: static compare(-2147483648, -2147483648)
Failed: static compare(0, -2147483648)
Failed: static compare(2147483647, -2147483648)
Passed: static compare(-2147483648, 0)
Passed: static compare(0, 0)
Passed: static compare(2147483647, 0)
Failed: static compare(-2147483648, 2147483647)
Failed: static compare(0, 2147483647)
Failed: static compare(2147483647, 2147483647)
Failed: static main(null)
Failed: static main({})

===============================================
Command line suite

Total tests run: 13, Failures: 8, Skips: 0

这个方法错误性很明显,而且看的出来,它的边界检查很有效啊,而修改后,程序通过了测试:

/*@ public normal_behaviour
  @ ensures \result == lhs - rhs;
*/
public static long compare(int lhs, int rhs) {
    long a = lhs;
    a = a - rhs;
    return a;
}

$ java -cp jmlunitng.jar Demo_JML_Test
[TestNG] Running:
Command line suite

Passed: racEnabled()
Passed: constructor Demo()
Passed: static compare(-2147483648, -2147483648)
Passed: static compare(0, -2147483648)
Passed: static compare(2147483647, -2147483648)
Passed: static compare(-2147483648, 0)
Passed: static compare(0, 0)
Passed: static compare(2147483647, 0)
Passed: static compare(-2147483648, 2147483647)
Passed: static compare(0, 2147483647)
Passed: static compare(2147483647, 2147483647)
2147483648
Passed: static main(null)
2147483648
Passed: static main({})

===============================================
Command line suite

Total tests run: 13, Failures: 0, Skips: 0

由于我们的作业比较复杂,其中重要的方法并不适合自动生成测试样例,所以可见JMLUnitNG还是有所局限,对于较为复杂的程序,有些hold不住了,那我们再来看看 JMLUnit:

以下是我对graph接口的相关方法进行简单的测试:

ublic class MyGraphTest {
    private static MyPath p1 = new MyPath(1, -1, 2, 2, 3);
    private static MyPath p2 = new MyPath(1, 2, 5, 2, 5, 6);
    private static MyPath p3 = new MyPath(3, 4, 5, 6, 7, 8);

    @Test
    public void containsEdge() {
        MyGraph myGraph = new MyGraph();
        try {
            myGraph.addPath(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        assertFalse(myGraph.containsEdge(1, 2));
        assertTrue(myGraph.containsEdge(2, 2));
    }

    @Test
    public void isConnected() {
        MyGraph myGraph = new MyGraph();
        try {
            myGraph.addPath(p1);
            myGraph.addPath(p2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            assertTrue(myGraph.isConnected(-1, 6));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void getShortestPathLength() {
        MyGraph myGraph = new MyGraph();
        try {
            myGraph.addPath(p1);
            myGraph.addPath(p2);
            myGraph.addPath(p3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            assertEquals(0, myGraph.getShortestPathLength(2, 2));
            assertEquals(5, myGraph.getShortestPathLength(1, 8));
            assertEquals(3, myGraph.getShortestPathLength(3, 6));
            assertEquals(4, myGraph.getShortestPathLength(7, -1));
        	assertEquals(1, myGraph.getShortestPathLength(2, 3));
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            myGraph.removePath(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            assertEquals(3, myGraph.getShortestPathLength(2, 3));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试结果如图,自己简单的测试是没有问题的:

img2

但是如果,仅仅是简单的测试,可能会有没有考虑到的情况,比如如果没有我在测最短路径时,对remove后的情况进行测试,可能又有隐藏的bug。

因此,对于比较复杂的程序,如果使用 Junit 进行测试,则需要充分考虑各种可能情况,来构造测试样例,比如if导致的不同结果分支等。

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

这个单元的作业让我们了解了JML语言,并学会了熟练使用相关设计思路和工具。而在使用过程中,所以设计策略,首先应该保证程序的正确性,其次再去保证性能的优化。首先,从需求来分析,给你请求,要求实现乘客的准确到达,这就需要电梯的正确调度和运行。

而第一次作业,我在结构设计上采用的是多容器来解决时间性能问题,即:

public class MyPath implements Path {

    private ArrayList<Integer> nodesList;
    private HashMap<Integer, Integer> nodeMap;	
    //map与list存的东西一样,但是map便于查找
    
}
public class MyPathContainer implements PathContainer {
    private HashMap<Path, Integer> pathMap;
    private HashMap<Integer, Path> pidMap;
    private int count;
    private HashMap<Integer, Integer> countMap;
    //左边key 右边为出现次数

}

第二次作业,path类,无需改变,而MyPathContainer,由于加带了图结构,所以,我在第一次作业的基础上进行了改进,将图结构存起来,而图的数据结构,我选择的是hashmap嵌套hashmap,而由于图没有权值,所以直接使用bfs来解决最短路径问题,具体如下:

public class MyGraph implements Graph {
    private HashMap<Path, Integer> pathMap;
    private HashMap<Integer, Path> pidMap;
    private int count;
    private HashMap<Integer, Integer> countMap;		//标记不同的点的次数
    private HashMap<Integer, HashMap<Integer, Integer>> graph;  //邻接矩阵
    private HashMap<Integer, HashMap<Integer, Integer>> disTable;   //距离矩阵
    private boolean ifcalcu;				//全局变量

}
public void bfs(int from) {
    Queue<Integer> que = new LinkedList<>();
    HashMap<Integer, Integer> map = new HashMap<>();
    map.put(from, 0);
    que.offer(from);
    while (!que.isEmpty()) {
        int top = que.poll();
        int distance = map.get(top);

        for (int key : graph.get(top).keySet()) {
            if (!map.containsKey(key)) {       //if !visited
                map.put(key, distance + 1);
                que.offer(key);
            }
        }
    }
    disTable.put(from, map);
}

第三次作业,保留了不少上一次作业的设计思路,正如,数据结构,hashmap嵌套hashmap,仍然与第二次相同。但由于图结构有不同权值,而且存在换乘,这导致上一次的bfs肯定要放弃了,改用dij来计算最短路径,但是我的算法仍然有些冗杂。因为我的设计是,每次加入的path,都构造成完全图,每次换一条边,相当于增加一次换乘。

public class MyRailwaySystem implements RailwaySystem {
    private HashMap<Path, Integer> pathMap;
    private HashMap<Integer, Path> pidMap;
    private int count;
    private int blockCount = 0;
    private HashMap<Integer, HashMap<Integer, Integer>> graph;  //邻接矩阵
    private HashMap<Integer, HashMap<Integer, Integer>> disTable;   //距离矩阵
    private HashMap<Integer, HashMap<Integer, Integer>> valueMap;	//以下各种权值矩阵
    private HashMap<Integer, HashMap<Integer, Integer>> unpleavalMap;
    private HashMap<Integer, HashMap<Integer, Integer>> changeMap;
    private HashMap<Integer, HashMap<Integer, Integer>> priceMap;
    private HashMap<Integer, HashMap<Integer, Integer>> unpleaMap;
    private GraphHelper helper;		//新的类
    private boolean valCalcu;
    private boolean chanCalcu;
    private boolean priCalcu;
    private boolean unpleaCalcu;
    private boolean disCalcu;
    private boolean bloCalcu;		//这些也是全局变量,决定是否更新某个图
}

可见,总体设计变得比较复杂了,每次add,remove要更新图结构,所以虽然增加了另一个类为graphHelper,不同于第二次的暴力融合在一个类里,但是方法仍然比较冗杂,这容易导致错误的产生。而具体的错误分析,则请移步下一板块啦。

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

前两次作业,从设计思路上来说,没有任何问题,强测和互测都是满分,原因是自己先用 JUnit 测试了程序的一定正确性,并用随机数据生成器,与他人进行对拍,充分的进行了测试,保证了正确性。而且前两次优化了容器的使用,使得设计本身不会导致超时等问题。

但是,最后一次作业却因为一行代码翻车,如下:

public void removeEdge(int from, int to) {
    if (!graph.containsKey(from)) {
        return;
    }
    HashMap<Integer, Integer> edges = graph.get(from);
    int num = edges.get(to);
    if (num == 1) {
        edges.remove(to);
        disCalcu = true;
        bloCalcu = true;
    } else {
        edges.put(to, num - 1);
    }
    if (edges!=null) {			//应该改为edges.size()!=0
        graph.put(from, edges);
    } else {
        graph.remove(from);
    }
}

这一错误是当一个hashmap的key全部remove了时,它并不是null的,而是应该用size为0来判断。
而这个错误导致我的强测和互测基本奔溃,为什么会出现bug呢?

原因是第三次我的设计思路比较迟缓,留给自己测试时间太短了,没有进行充分的测试,这个方法第二次作业也有,但是在其他方法使用时,对容器进行了一定改变,我没有充分测试另一个方法的各种情况,导致没有发现调用时的问题。

另外,测试手段真的很关键,随机数据生成器也不能依赖,应该仔细分析程序的各种可能结果,自己精心构造好测试样例,而构造好测试样例,不仅是在动手前,也是在动手后的仔细检查中。

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

规格的撰写,是有利于设计者、开发者,有利于团队合作的,代码规格能够帮助我们对代码的逻辑思路、代码风格进行规范和引导,在浏览规格时,能给开发者予以启迪,也能给合作者快速理解代码的可能。我想,在大型的工程或团队里,共同维护良好的规格设计和代码风格等工作,也是比不课少的。

而我自己在使用中也体会到了JML语言的好处,JML帮助我理清楚设计思路,帮我检查程序正确性,openJML,Junit,JunitNG等工具的使用也让我看到了写规格化设计的优势,之后,我会多使用这些工具,增加对JML及其工具链的熟练使用。

posted @ 2019-05-22 21:51  Puzzled_Bubble  阅读(129)  评论(1)    收藏  举报