OO第三单元总结
经历了3周的学习,现对第三单元做一个总结。
一.梳理JML的语言基础和应用工具链情况。
(1)JML语言理论基础。
第三单元的任务主要是基于JML规格实现某些类中的某些方法,因此,读懂JML就成了完成此次作业的第一步。现对本单元比较常用的JML表达式做一些梳理。
语句部分:
- \old(expr)表达式:用来表示expr再放映方法执行前的取值。值得注意的是,作为一般规则,在使用该方法时,应用括号把expr作为整体括起来。例如:
![]()
- \result表达式:表示一个非void类型的方法执行所获得的结果,即方法执行后的返回值。\result表达式的类型就是方法申明中定义的返回值类型。例如:
![]()
- \forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
- \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应约束。
- \num_of表达式:返回指定变量中满足相应条件的取值个数.
方法规格:
- 前置条件:通过requires子句表示:requires P;。前置条件是调用者调用当前方法时当前环境必须满足的条件,但是在当前方法中不需要实现该判断。注意同一个方法的正常功能的前置条件和异常功能的前置条件一定不能重叠,否则会引法严重的设计错误。根据LSP原则,任何基类出现的地方,都能用子类去替换,因此如果B继承A,在相应方法的前置条件处,可以放宽requires的条件。
- 后置条件:通过ensures子句来表示:ensures P;。表明方法实现者确保方法执行返回结果一定满足谓词P的要求。规格中可以有多个ensures子句,方法实现者必须同时满足所以ensures子句的要求。根据LSP原则,如果B继承A,在相应方法的后置条件处,可以缩紧ensures的要求。
- 副作用范围限定:用assignable或modifiable表示,指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。在继承时,由于父类看不到子类中新增的属性,因此即使子类中因为对额外属性进行操作导致父类与子类中的assignable不一样,只要父类中的属性值变化相同,则也是正确的。
- also子句:两种使用场景:(1)父类中相应方法定义了对各,子类中邪恶该方法,需要补充规格,这是应在补充的规格之前使用also;(2)一个方法规格中涉及多个功能规格描述,正常功能规格或者异常功 能规格,需要使用also来分隔。
- signals子句:signals(***Exception e) b_expr。意思是当 b_expr 为 true 时,方法会抛出括号中给出 的相应异常e。注意, 如果一个方法在运行时会抛出异常,一定要在方法声明中明确指出(使用Java的 throws 表达式),且必须确保 signals子句中给出的异常类型一定等同于方法声明中给出的异常类型,或者是后者的子类型。
规格类型
- 不变式invariant:invariant P。不变式是要求在所有可见状态下都必须满足的特性,凡是会修改成员变量(包括静态成员变量和非静态成员变量)的方法执行期间,对象的状态都不是可见状态。换句话说,在方法执行期间,对象的不变式有可能不满足。因此,类型规格强调 在任意可见状态下都要满足不变式。不变式中可以直接引用pure形态的方法。
- 状态变化约束constraint:constraint P。constraint用来对前序可见状态和当前可见状态的 关系进行约束。invariant和constraint可以直接被子类继承获得。
(2)应用工具链情况
JML编译器,如OpenJML。
-OpenJML的一个基本功能就是对JML注释进行检查,包括经典的类型检查、变量可见性与可写性等检查。通过Check——Run Check即可进行类型检查。(对于注释编译的报错和警告信息,笔者每次都觉得很棘手,经常不知道该如何下手进行修改(′д` ))
-check只能检查注释语法的正确性,如果想对规格内容进行检查,需使用-esc参数,如果是静态检查,需要指定prover。
-使用-rac选项可以执行运行时检查。JML UnitNG结合OpenJML可以生成一个java类文件测试的框架,实现对代码的自动化测试。
JMLdoc,与javadoc类似,但其生成的Html格式文档中包含JML规范。
二.部署SMT Solver
跳过。
三.部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例,并结合规格对生成的测试用例和数据进行简要分析
在src中放入以下三个内容:
-jmlunitng.jar
-testng-6.8.7-sources.jar
-Demo.java
笔者先使用讨论区中大佬给的Demo试了一次:

然后在src中通过命令行输入:

在idea中直接打开src包,运行Demo_JML_Test即可。
笔者生成的测试数据如下:

(貌似每个人都一样?捂脸)
然后笔者尝试对Graph接口自动生成测试样例,但不知是因为Graph接口需要调用到类,还是什么原因,bash上一堆报错,因此改写了第一次作业的Mypath类,仍命名为Demo如下:
public class Demo {
private int[] nodes = new int[1024];
public Demo(int... nodeList) {
for (int i = 0; i <= nodeList.length - 1; i++) {
nodes[i] = nodeList[i];
}
}
public int getNode(int index) {
return nodes[index];
}
public int size() { //1
return nodes.length;
}
public static int compareTo(Demo o1, Demo o2) {
int flag = 0;
for (int i = 0; i <= o1.size() - 1 && i <= o2.size() - 1; i++) {
if (flag != 0) {
break;
}
if (o1.getNode(i) > o2.getNode(i)) {
flag = 1;
} else if (o1.getNode(i) < o2.getNode(i)) {
flag = -1;
}
}
if (flag == 0) {
if (o1.size() == o2.size()) {
flag = 0;
} else if (o1.size() > o2.size()) {
flag = 1;
} else {
flag = -1;
}
}
return flag;
}
public static void main(String[] args) {
int[] a = {1, 1, 2, 3};
Demo b = new Demo(a);
int[] c = {1, 1, 2, 4};
Demo d = new Demo(c);
compareTo(b,d);
}
}
按照上述步骤生成了自动测试样例:

(这样例看着好奇怪)
四.对三次作业的架构分析
(1).第一次作业
类图如下:

官方接口长啥样,我的就长啥样。
其中MyPath类中使用ArrayList记录节点序列,并设一个HashMap记录路径中编号为id的节点的出现次数,以便在ContainsNode()中直接判断某个节点是否存在,复杂度为O(1);
MyPathContainer中有三个HashMap,map1、map2分别用于存储路径以及其对应的id,方便在根据path找id和根据id找path能够以复杂度为O(1)的方式完成任务,map3用于记录节点及其出现的次数,通过数据冗余,使getDistinctNodeCount()方法的复杂度降低为O(1).
(2).第二次作业
类图如下:

第二次作业仍然是接口长啥样,我的类就长啥样。虽然觉得CtrlV使程序稍显臃肿,但懒惰让我选择了CtrlV。
MyPath类和上一次完全相同。
MyGraph继承Graph接口,Graph接口继承PathContainer。由于这种继承关系,因此MyGraph中需要实现PathContain中的方法,除此之外,还需实现Graph接口中的方法。笔者的第一版代码选用dij算法,但由于对dij算法感到比较陌生,在邻接矩阵初始化时没能很好的理清关系,因此写出了bug,考虑到复杂度的问题,后来采用bfs算法计算两点间的最短距离,除了第三次作业完全重构外,bfs的在计算两点间最短路径的复杂度和算法写起来的复杂度相比dij都友善的多。mapGraph用于记录图中存在的边,mapWeight用于记录两点间距离,containsEdge()方法通过mapGraph判断,getShortestPathLength()中要先使用isconnect()方法判断两点间是否相连,因此在isconnect()中先查找mapWeight是否存在相应边,否则使用bfs计算两点间距离,并且,为了进一步降低复杂度,可以在mapWeight中保存bfs计算过程中得到的中间值,getShortestPathLength()中可以直接在mapWeight中查找相应边。
(3).第三次作业。
类图如下:

抽象出GraphMethod1和GraphMethod2作为图方法。
第三次作业,要构建4个图,由于我采用的是拆点的写法,因此最低票价、最低不满意度的图和最少换乘、最短路径的图类型不同,因此笔者抽象出了两个图graphMethod1(用于最少换乘和最短路径)、graphMethod2(用于最低票价和最低不满意度), 总算不是无脑CtrlV(当然,checkstyle也不允许我无脑CtrlV了),但还是没用上继承,稍感欠缺。
MyRailwaySystem里维护myGraph用于记录图中包含的边。
GraphMethod1里维护map1用于记录含有的path,map3用于记录含有的节点,mapWeight用于记录两点间距离,nodeWeight和visited用于dij,MyRailwaySystem中调用iniMapWeight1(或iniMapWeight2,用于最少换乘)初始化mapWeight,笔者在GraphMethod1中同时写了两个初始化mapWeight的方法,为了适应最短距离和最少换乘在初始化图时的不同,后来想了想,此处直接创建一个最少换乘的类,该类继承自最短距离,并改写初始化mapWeight的方法,会更好。GraphMethod1中建立缓存,addPath时并不算出每个节点到其他节点间的距离,在MyRailwaySystem中需要使用getShortestPathLength或getLeastTransferCount时,先在缓存中寻找,缓存中不存在在调用dij算出某点到其他所有节点的距离,然后将中间结果存入缓存,并返回所求数据。这里注意在path有变化时缓存都需要重新清空。
GraphMethod2里维护mapGraph(HashMap<Node, HashMap<Node, Integer>>)和mapWeight(HashMap<Node, HashMap<Node, Integer>>),mapGraph用于记录Path中存在的边,mapWeight用于记录两点间距离,由于使用拆点的方式,因此不同path中id相同的点也被认为是不同点,正是因为这个性质,addPath和removePath时都可以只动一条边,因此笔者没有像GraphMethod1一样省去mapGraph,而是选择保存mapGraph,仅在addPath和removePath时改变mapGraph,同时mapWeight的初始化就好像复制了一份mapGraph。然而由于没有处理好path中的相邻节点的问题,因此强测爆冷,在removePath的时候出现了空指针。此外,拆点方式的不正确选取导致笔者的程序复杂度很高,强测大面积出现tle。笔者并非采用菊花点算法,而是在每次addPath时将该path上的点与path中左右节点相连,并于mapGraph中存在的节点id相同的节点连起来,权值赋为2或32,在removePath时,按反过程进行拆边即可。(这里建议在getKey、getValue或remove前先判断一下对象是否存在,笔者在写程序时没有考虑清楚重复点问题,认为想取的值一定存在,然而确实有时是不存在的,只要get之前contains一下,就一定不会出现空指针错误)这里笔者还是没用到继承,现在想想,应该使用继承机制,创建最低票价和最小不满意度的类,最小不满意度类继承自最低票价,同时改写addPathToMapGraph和iniMapWeightAdd方法。
五.分析bug和bug修复情况。
第一次作业:
第一次作业笔者在强测、互测中均没有出现过bug。但笔者进行过一次重构,按照原来的写法,MyPathContainer中的getDistinctNodeCount复杂度为O(n*n),cpu时间为10s,有点捉急,遂采用数据冗余的方式,将getDistinctNodeCount复杂度改为O(1),顺利过关。同时,考虑到各个方法的复杂度问题,笔者第一次查看了源码(好像开始的太迟了QwQ),发现HashMap中containsKey和containsValue的复杂度不同,因此在所有containsValue能改为containsKey的地方都做了相应的修改。在互测中,看了本组大部分同学的代码,均没有发现错误,然而互测结束后,发现有个组员居然被hack了,空指针导致程序出现了异常,看来笔者思维还是不够缜密,果不其然,笔者作业第三次出现了空指针问题。
第二次作业:
本次作业笔者在强测和互测中均没有发现bug。同理,笔者还是进行了一次重构,将原本写的dij换为了bfs,算法好写,跑的还快,就是对于第三次作业毫无扩展性。
第三次作业:
强测凉凉,互测凉凉,提交了重构的代码,然而复杂度还是过高,一个被同学hack的点至今还是tle。笔者近期比较忙,加之苦于没有思路,因此比较迟才开始写,写完第一版已经是周一晚上,由于没有认真研究过复杂度,因此周二下午知道自己的程序跑的有多慢的时候,就知道强测凉凉了,对于再次逻辑验证程序的正确性就有点懈怠,加之写程序的时候对于remove没有考虑重复删除节点的情况,因此出现了空指针的问题。强测结果出来后笔者对程序进行了重构,采用讨论区中wjy大佬说的不拆点,在一条path中直接算出两点间的距离,将换乘的票价或不满意度直接加到边中,然后将边加入大图,接着使用正常的dij,在调用getLeastTicketPrice和getLeastUnpleasentValue返回得到结果时减去2或32。因此笔者修改了GraphMethod2中初始化mapWeight的方法,并且对dij进行了小小的修改。
观察修改后的程序,发现四个图的结构已经大致相同,只是部分方法存在区别,因此可以通过创建一个最短路径的类,其余三个类继承自最短路径类,并修改相应的方法即可。同时,笔者在自己测试时还发现对于传入函数的参数,如果函数外参数改变了,函数内部参数也会改变,大概是由于是指针的原因导致的吧,可以通过因此笔者重写了clone函数(在笔者的程序里叫duplicate),传了一个参数的副本进函数,并且在适当的时候对副本的值进行改变。并且,笔者的Edge类里还忘了重写equal方法,通过单步调试看了两小时都没看出出错的逻辑在哪里,后来求助同学通过打印语句才找出这个bug。
六.对规格撰写和理解上的心得体会。
JML是严谨的语言,其通过离散数学一样的式子,对代码的功能进行描述,避免了自然语言描述代码时产生的二义性问题,就是在撰写规格时,一些神秘的语法报错时常让我摸不着头脑,因此也不太清楚要怎么修改。
现在谈谈我对第三单元的体会。在第三单元之前,我没怎么考虑过程序的复杂度问题,虽然电梯单元也涉及到时间,但电梯里只要让乘客尽可能地上电梯就好了,因此感觉电梯里时间不是主要问题。但是这一单元,对cpu时间的限制,让我不得不思考复杂度的问题,通过源码查看方法的复杂度,为了降低复杂度选择合适的容器与算法并适当作冗余数据的存储,在有多种算法通过计算不同指令的数量决定算法的选取情况,总之,本单元一直与数据结构和复杂度打交道。但笔者并没有因此觉得本单元就不oo了,java为我们提供了众多数据结构,如ArrayList、LinkedLlist、HashMap、HashSet等众多容器,供我们根据自己的需求使用,本单元我们相当于封装了一个Graph容器(算是从事开发OO比较底层的开发工作吗?),符合oo封装对象的思想,只是没怎么体现对象间的信息交流罢了。本单元除了第三次强测略感遗憾之外,也对自己作业的架构不太满意,如前所述,笔者虽然抽象出了图类,但是没有合理运用继承,因此在设计架构方面,还有较大的提升空间。



浙公网安备 33010602011771号