OO第三单元总结

2019面向对象课程序设计第三单元总结

在面向程序设计第三单元的作业中,我们开始学习jml语言。jml是java建模语言(Java Modeling Language)的缩写,是一种可以进行详细设计的符号语言。jml语言给了我们一个新的角度去看待Java。通过jml语言去描述一个方法,能够减少自然语言表述带来的歧义,另外,jml语言也强调了在面向对象的分析和设计的一个重要原则:过程性的思考应该尽可能地推迟。比如在求最短路径时,首先想到的不应是采用什么算法,而应先定义好何谓最短路径。通过jml语言显示的设计,可以减少后续的不少麻烦。

一、JML语言的理论基础

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

一般而言,JML有两种主要的用法:
(1)开展规格化设计。使用逻辑严格的规格而不是可能带有内在模糊性的自然语言描述,能够使代码实现过程更加容易。JML的有些语法有点类似离散数学里学到的逻辑表达式,通过没有歧义的方式规定方法的前置条件、后置条件、副作用范围限定,能够保证方法执行的准确、安全。
(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。在多人合作的项目中,读懂别人代码是一件挺困难的事。另外,如果代码没有严谨的描述,就很有可能因为理解上的偏差造成不必要的错误,而且这一类错误往往很难复现。

二、JML应用工具链

在完成JML以及对应的java代码之后,可以借助工具来判断代码是否符合JML规格。JML的测试方式很多,可以使用openJML进行测试,也能使用JMLUnitNG对代码进行测试。

OpenJML

OpenJML可以实现对JML注释正确性的检查,可以用openJML进行运行时检查,JML语法静态检查,程序代码静态检查

运行时检查

java -jar openjml.jar -rac test.java

JML语法静态检查

java -jar openjml.jar -check test.java

程序代码静态检查

java -jar openjml.jar -esc test.java 

三、JMLUnitNG

测试程序代码如下:

public class Main {
    public static void main(String[] args) {
        add(1, 2);
        mul(30, 23);
    }

    /*@ public normal_behaviour
      @ ensures \result == x + y;
    */
    public static int add(int x, int y) {
        return x + y;
    }

    /*@ public normal_behaviour
      @ ensures \result == x * y;
    */
    public static int mul(int x, int y) {
        return x * y;
    }
}

测试步骤:

1.生成文件

java -cp jmlunitng.jar Mian.java

2.编译

javac -cp jmlunitng.jar *.java
java -cp openjml.jar -rac Main.java

3.测试

java -cp jmlunitng.jar Main_JML_Test

测试结果如下:

四、架构设计

第一次作业

第一次作业的架构并不需要太多的设计,只需要根据JML规格完成设计就可。但需要注意的是,强侧的数据量比较大,如果采用数组进行查找的话,复杂度会很高,需要使用其它容器保证运行速度。以下是我在作业过程中采取的方法:

1.尽量使用HsahMap,避免使用数组在查找时的循环操作

private HashMap<Integer, Path> plist;
private HashMap<Path, Integer> idlist;
private HashMap<Integer, Integer> count;

2.使用互为键值对的两组HashMap来表示PATH和PATHID。虽然在添加PATH和删除PATH时需要多一步操作,但是由于HsahMap无法通过value值直接获得key值,所以只有使用双HashMap才能避免查找时的循环操作。

3.DISTINCT_NODE_COUNT的问题,要降低DISTINCT_NODE_COUNT指令的时间复杂度,就需要在建立一个以节点值为key值,节点出现次数为value值的HashMap,当value值减少到0时,则删除该节点。该map的大小就为节点个数。

第二次作业

 从第二次作业开始,就需要多考虑架构设计,但可惜的是,在本单元的作业中我的架构设计得并不好。在第二次作业中,我采取的是Floyed算法。选择在改变图结构时,就计算出所有点之间的最短距离。因为变更图结构的指令较少,而数量较多的查找指令的复杂度只有O(1)。

        for (int k : count.keySet()) {
            for (int i : count.keySet()) {
                for (int j : count.keySet()) {
                    Node ik = new Node(i, k);
                    Node kj = new Node(k, j);
                    Node ij = new Node(i, j);
                    int length;
                    if (!map.containsKey(ik) || !map.containsKey(kj)
                            || map.get(ik) == Integer.MAX_VALUE
                            || map.get(kj) == Integer.MAX_VALUE) {
                        length = Integer.MAX_VALUE;
                    } else {
                        length = map.get(ik) + map.get(kj);
                    }
                    if (!map.containsKey(ij) || map.get(ij) > length) {
                        map.put(ij, length);
                    }
                }
            }
        }

 第三次作业

 由于在第二次作业中优化不够好,所以第三次作业我尝试重构代码,采取的是堆优化的dijkstra算法,将计算出来的节点保存,以便下次查找。代码段如下:

        Queue<Edge> que = new PriorityQueue<>();
        HashMap<Integer, Integer> temp = new HashMap<>();
        temp.putAll(shortestPath.get(fromNodeId));
        ArrayList<Integer> visit = new ArrayList<>();
        for (int i : temp.keySet()) {
            que.add(new Edge(i, temp.get(i)));
        }
        while (!que.isEmpty()) {
            Edge now = que.poll();
            int u = now.getTo();
            if (temp.containsKey(u) && temp.get(u) < now.getDist()) {
                continue;
            }
            visit.add(u);
            HashMap<Integer, Integer> utemp = shortestPath.get(u);
            for (int i : utemp.keySet()) {
                if (!temp.containsKey(i)) {
                    temp.put(i, now.getDist() + utemp.get(i));
                    que.add(new Edge(i, now.getDist() + utemp.get(i)));
                }
                if (temp.get(i) > now.getDist() + utemp.get(i)) {
                    temp.put(i, now.getDist() + utemp.get(i));
                    que.add(new Edge(i, now.getDist() + utemp.get(i)));
                }
            }
        }

 总的来说,我在这个单元作业的最大的问题就在架构设计上,由于完成第二次作业的时候,并没有在架构设计上多下功夫,也没有考虑到后续的扩展,所以第二三次作业完成得都不太理想。也从反面印证了架构设计的重要性。

五、代码实现的bug和修复情况

第一次作业

由于第一次作业相对简单,所以并没有在强侧中出现bug,所以写一下自己在代码实现过程中注意到的一些点(也是很多人都做了的优化):尽量避免循环,降低时间复杂度。

第二次作业

第二次作业强测中出现了CPU_TIME_LIMIT_EXCEED的错误,确切的说在强侧截至之前就已经发现,但是时间已经不允许对代码的修改。这一次的作业是求最短路径,我才用的是Floyd算法,由于在测试数据中,变更图结构的指令相对较少。所以我在每一次执行PATH_ADD或者PATH_REMOVE操作,变更图结构时,求出所有点之间的最短路径长度。而执行查询指令时只需在已计算的结果里查询便可。但是由于在算法实现过程中,优化不够:每一次变更图结构都需要在现有节点基础上重新计算,导致当节点个数到达150个左右时执行速度很慢。

第二次的bug修复是和第三次作业同时进行的,所以对代码经行了大幅的重构。这一次采用了堆优化的dijkstra算法。因为在求单源最短路径时求出来的路径不止fromNode和toNode一条,而是从fromNode到其余个点的长度,所以可以将这些长度都缓存起来,后续再查询或者经过时可以直接使用,降低代码的时间复杂度。

第三次作业

这一次作业的完成情况很差,由于第二次作业的重构,导致第三次作业的开始时间延迟,加上代码架构的问题,导致第三次作业结果惨不忍睹。主要体现在:

1.代码正确性的问题,有换乘的最小换乘数、最少价格、最少不愉悦度。

2.代码架构问题,此次出现了大量的TIME_LIMIT错误,使用dijkstra算法时没有考虑优化。

六、心得体会

在本单元的作业中,一直在强调架构问题,经过三次作业的练习,在如何架构设计上有了一点进步,也吸取了不少教训。在进行架构设计时要尽可能多的包括所有情况,思考所有例外的情况应该如何处理,如果在开始实际编码之后再一遍思考一遍完成的话,很有可能导致自己的架构设计得一塌糊涂。

在架构设计时应该尽可能的考虑到代码的扩展性问题。以我们的OO作业为例,每个单元的三次作业之间都是相互联系的,如果一次的架构设计出了问题,后续的作业就可能花上两倍三倍的时间去重构,造成不必要的时间浪费,而且正确性的保障也会大大降低。

最后是关于本单元作业的总结。第一次作业难度不大,只需要根据规格说明,选择合适的容器就能保证强侧顺利通过。第二次作业需要在架构设计以及算法选择上下功夫。也正是因为在第二次作业中架构出了问题,算法优化不够好,导致强侧并不理想,而且严重影响了第三次作业。第三次作业没有解决第二次在架构上的问题,所以出现的错误也越来越多,大有拆东墙补西墙的感觉。剩下的面向对象作业还有一个单元,希望会有更好的发挥。

posted @ 2019-05-21 22:41  17373462  阅读(203)  评论(0编辑  收藏  举报