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作业为例,每个单元的三次作业之间都是相互联系的,如果一次的架构设计出了问题,后续的作业就可能花上两倍三倍的时间去重构,造成不必要的时间浪费,而且正确性的保障也会大大降低。
最后是关于本单元作业的总结。第一次作业难度不大,只需要根据规格说明,选择合适的容器就能保证强侧顺利通过。第二次作业需要在架构设计以及算法选择上下功夫。也正是因为在第二次作业中架构出了问题,算法优化不够好,导致强侧并不理想,而且严重影响了第三次作业。第三次作业没有解决第二次在架构上的问题,所以出现的错误也越来越多,大有拆东墙补西墙的感觉。剩下的面向对象作业还有一个单元,希望会有更好的发挥。