OO_Unit3_Summary
第三单元作业相比于前两次作业花费的时间要明显少一些,因为更多的是依据jml语言去实现相应的要求,而且所设计的比较难的功能不需要自己花很多时间和精力思考,而是可以在博客上寻找对应的算法来学习,所以我认为难度相比于之前的两次还是降低了,但是也同样因为难度降低而放松了本地测试,出现了一些意外的bug,这一点值得反思。
JML语言基础和工具链
JMl(JAVA Modeling Language)是用于对java程序进行规格化设计的一种表示语言,一种行为接口规格语言。可用于规格化设计和针对已有代码书写规格提高可维护性。
-
一个正常的 JML 规格往往可以包含下面几个部分:前置条件、后置条件、副作用范围限定、不会改变的元素和属性、不正常的行为发生时抛出异常;
-
JML 中常用的表达式:
-
\old(expr)
表达式用来表示一个表达式expr
在相应方法执行前的取值; -
\result
表达式表示方法的执行返回结果; -
\forall
表达式是全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束; -
\exists
表达式是存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束; -
\sum
表达式表示返回给定范围内的表达式的和; -
\product
表达式表示返回给定范围内的表达式的连乘结果; -
\max
\min
表达式分别表示返回给定范围内的表达式的最大值和最小值; -
<==>
<=!=>
等价关系操作符:b_expr1<==>b_expr2
或者b_expr1<=!=>b_expr2
分别表示表达式相等或不等; -
==>
<==
推理操作符:b_expr1 ==> b_expr2
或者b_expr2 <== b_expr1
。
-
-
JML 中常用方法规格:
-
前置条件
requires
:requires P
表示要求调用者确保 P 为真; -
后置条件
ensure
:ensures P
表示方法实现者确保方法执 行返回结果一定满足谓词P的要求,即确保 P 为真; -
副作用范围限定:关键词
assignable
表示可赋值,modifiable
表示可修改; -
pure
:表示方法是纯粹的访问方法,不会对对象进行任何修改; -
signals:其结构为signals(Exception e) b_expr,意思是当b_expr为true时,会抛出相对应的异常e;
-
-
JML中常用类型规格
-
不变式invariant:要求在所有可见状态下都必须满足的特性,语法定义为invariant P;
-
状态变化约束constraint:对对象状态的变化进行约束。与invariant的区别是invariant只对可见状态约束,而constraint可以对前序可见状态和当前可见状态的关系进行约束;
-
-
-
JML 的工具链有以下几种:
-
OpenJML 用于验证模块是否完成 JML 规定的功能;
-
JMLlauncher 图形用户界面工具的启动器;
-
JMLdoc 包含 JML 规范信息的 javadoc 版本;
-
JMLUnit 用于自定义测试模块是否符合需求;
-
(以上部分引用自https://www.cnblogs.com/delicate1989/p/10902380.html)
SMT Solver的使用
SMT Solver一开始在百度上搜了半天,没找到下载的位置,后来发现实际上包含在openjml下载的压缩文件里。
SMT solver可以对JML的语法进行检查,也可以用静态检查和运行时检查来检查规格内容,于是我进行了如下的测试。
JML静态检查
这里我只对Group.java和Person.java进行了检查
Person通过了,但Group的检查好像是由于不支持三目运算符出现了两个错误。
运行时检查
运行时检查爆出一个异常,然而这个异常说的什么我也没看懂,自己在网上找了半天也没找到如何解决,询问大佬后大佬也不知道如何怎么办,索性只能放弃了。
语法检查
语法检查的错误内容和静态检查的完全相同...,都是无法识别三目运算符
后来我发现openjml还提供了网页的静态检查检查,于是我把Person.java提交了上去,顺利的通过了检查,但是这个网页貌似只支持一个java文件的检查(我把Group.java和Person.java同时复制上去就超时了...)
网址附上:https://www.rise4fun.com/OpenJMLESC/
JMLUnitNG
这个JMLUnitNG把我按在地上摩擦了两个小时后我屈服了,两个小时内我见识了满屏 的报错,于是我放弃了对MyGroup的测试,自己写了一个比较简单的Person类进行了测试,
MyPerson代码如下
package test; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; public class MyPerson implements Person { private int id; private String name; private BigInteger character; private int age; private int groupId = 0; public MyPerson(int id, String name, BigInteger character, int age) { this.id = id; this.name = name; this.character = character; this.age = age; this.acquaintance = new ArrayList<>(); } public int hashCode() { return Objects.hashCode(id); } public int getId() { return this.id; } public String getName() { return this.name; } public BigInteger getCharacter() { return this.character; } public int getAge() { return this.age; } }
java -jar jmlunitng-1_4.jar test/*.java javac -cp jmlunitng-1_4.jar test/*.java java -jar openjml.jar -rac test/*.java java -cp jmlunitng-1_4.jar test/Person_JML_Test
输入这些命令后得到了
测试结果如下:
可以看出如果是方法的参数为int,传进去的参数只有0,int的最大值,int的最小值
如果方法的参数是对象,就传入null
如果参数是字符串类型,就传入一个空字符串
架构设计和Bug分析
HW9
第一次JML作业比较简单,关键是先学习JML语言的语法,看懂接口中的规格,然后只要照着规格将一点点将方法补全即可。这次作业中最难的方法是iscircle,是用来找到两个点是否联通,所以只需要一次bfs即可。
UML
复杂度分析
My Bug
这次作业出现了一个bug,由于我阅读JML规格不仔细,在person的queryvalue方法,如果查询的人是自己就应该返回值为0,但我没注意到这一点返回的是容器people里对应的value,由于person自己没放入people,所以得到NullpointerExcepiton,最后强测崩了三个点,
Hack Bug
这次作业的难度实在不高,我想当然的认为大家都不会去犯错,所以就没有hack bug。
HW10
HW10在之前作业基础上增加了一个新的Group类,同时对运行时间和复杂度也添加了限制,完全按照JML规格写的话,大概率会超时,所以我们完成作业的思路不能仅仅依葫芦画瓢,而是需要自己想一些方法来优化,降低复杂度。
这次作业有一定难度的方法有:
-
isCIrcle:由于这次添加了对时间的限制,bfs的复杂度是O(V+E),所以很有可能会超时,所以我使用了并查集算法,新建了一个Block类,其中维护了一个”小弟-老大“的Map,并查集算法的核心将两个人是否属于同一个强连通分量转化为两个人是否拥有同一个“老大”即如果两个人是拥有同一个”老大“那么iscircle则为true,算法的大致过程是每次addPerson,都会往Map里新增一个entry,键和值都是自己,每次addRelation首先检查addRelation的两个人p1和p2是否是一个”老大“,如果不是就调用mergre方法,就在Map里添加一个entry
,还有一个值得注意的地方就是,寻找”老大“时需要记录沿途的人,待找到最后的老大后,将记录的人的老大更新。 -
queryGourpValueSum、queryGroupAgeMean、queryGroupAgeVar、queryGroupConfilctSum、queryRelationSum:
这几个方法的复杂度是O(n)或者O(n^2),比较容易超时,所以需要在每个group中维护size、ageSquare、ageSum、Confictsum、valuesum、relationsum几个属性,每次addToGroup和addRelation时对这几个属性进行维护和更新,其中重点说说value和方差。
value的特殊在于容易忽视先addToGroup后addRelation的顺序带来的改变,尤其是当这两个person同时在多个group时就相对复杂很多,因为首先需要记录person'所在的组,其次需要对其所在的所有组的属性进行判断、修改,而我的优化方法受到计组里的独热码启发,person所在的组都采用独热码来记载,例如一个person在第一和第四组,那么其所在的独热码就是1001,这样在addRelation,只要将两个人的独热码&就可以知道他俩同时所在的组是了。
方差的特殊性在于,由于方差和平均值都是是一个向下取整的整数而不是小数,方差计算公式E(X^2)-E(X)^2的失效,所以在讨论区大佬的指点下将方差公式修正为
var=(agesquare-2* mean* agesum +size * mean * mean)/ size;
就能够满足了原来的需求。
UML
复杂度分析
My Bug
这次作业自己吸取了上次的经验,提前构造了一些测试点自己测试,所以没有出现bug
Hack Bug
这次我hack bug的策略主要针对超时来进行,按照互测的数据的上限构造了一个尽可能复杂的数据,刀到了两个没有优化到位的同学。
HW11
第十一次作业除了可以在group里添加人以外还可以在group里删除人(课程组还是比较仁慈,不是在network里删除人),删除person的过程中也要维护之前所说的那几个group属性,大部分属性维护很简单,这里只说一个confilctsum,它的维护可以用的离散数学的知识
(A^B)^B =A^ (B^B)=A^0=A
所以在维护conflictsum时就有
conflictsum=confilctsum^age
还增加的就是money属性和borrowfrom方法,这个比较简单在network维护一个hashmap或者给每个person添加money属性即可,因此不再赘述。
此外这次作业里最难的两个算法就是queryBlockSum,queryMinPath 和queryStrongLink。
-
queryBlockSum:
如果上一次作业实现了并查集的话就非常简单,因为这个方法就是在询问联通块的个数,所以在Block里增加一个blocksum属性,addPerson时blocksum++,addRelation时如果需要merge合并时block--,queryBlockSum直接返回blocksum的值即可。
-
queryMinPath:
这个方法是需要我们求出两个点直接之间的最短路径,使用dfs遍历所有的可达路径固然可行,但是可能会超时,于是乎只能请出我们的dijkstra算法,这个算法具体的就不介绍了,离散和数据结构课上都有介绍过,算法中的优先队列使用java内置的容器PriorityQueue即可,不过需要自己定义好comparable的接口。
Dijkstra的具体算法见此链接(https://blog.csdn.net/lbperfect123/article/details/84281300)
-
queryStrongLink
这次作业里最让人头疼的就是这个方法,这个方法需要我们找到两条不同的路径连接两个人(这里的不同指的是两条路径不能有相同的点),经过查阅资料可知,实际上就是判断两个人是否处于同一个点双连通分量。而求点双连通分量最有效的算法之一就是tarjan算法,也是我这次作业所采用的算法。
tarjan算法里有几个概念需要提前说明
搜索树:在一张无向连通图中选定任意一个节点进行深度优先遍历,每个点仅访问一次。所有发生了递归的边会构成一棵树,我们称其为无向连通图的“搜索树”。
dfn(时间戳):我们对一张图进行深度优先遍历,根据第一次访问到它的时间顺序给它打上一个标记,这个标记就是时间戳
low(追溯值):我们用subtree(x)表示搜索树中以x为根的子树,low[x]定义为下列节点的时间戳的最小值:
-
subtree(x)中的节点
-
通过一条不在搜索树上的边,能够达subtree(x)中的节点
割点:一个结点称为割点(或者割顶)当且仅当去掉该节点极其相关的边之后的子图不连通。两个点双连通分量的公共点就是一个割点。并且在这里割点满足
1、根结点u为割点当且仅当它有两个或者多个子结点; 2、非根结点u为割点当且仅当u存在结点v,使得v极其所有后代都没有反向边可以连回u的祖先(u不算), 可以写为low[v] >= dfn[u]。
所以很容易可以发现,当我们找到一个割点的时候,就已经完成了一次对某个极大点双连通子图的访问,那么如果在进行DFS的过程中将遍历过的点保存起来,就可以得到这个点双连通分量了。
所以tarjan算法的具体实现就是首先对这个图进行dfs,dfs的过程中不仅要对每一个遍历的点维护好dfn和low,还有维护一个栈保存遍历过的边,每遇到一个割点u即满足上述割点判定规则,就不断弹栈直到遇到遇见边(u,v),这些弹出来的边包含的点够成一个双连通分量。
-
UML
复杂度分析
My Bug
这次作业虽然强测没爆,但是是运气所致,我的group属性里用的是int保存agesum和agesqaure,这次部分强测点的数据过大,用int保存这两个数据会导致agesqaure和方差公式里的mean*agesum的溢出,然而虽然我的这两个属性都溢出了,但是方差公式里两者作的是相减,所以溢出的效果被抵消了......,正确的做法是两个都应该用long来存储。
Hack Bug
这次作业我依然想用庞大的数据让别人超时,但好像大家都优化的很好,所以这次作业我没有hack到bug,但是同房间的同学却找出了另外一个同学的queryStronglink的bug,bug的方法时使用两次bfs实现querystronglink,第一次bfs后如果不联通返回false,反之,则删除第一次bfs访问的点,再次bfs,如果这时仍可以联通,则为true,反之为false,这种方法实现其实是错误的,因为第一次bfs找到的路径可能不是正确的两条不同的路径之一,盲目的删去第一次bfs的点,可能会导致正确路径的点被删去,从而第二次bfs无法联通。
心得体会
这一单元作业自己的成绩还不错,虽然有可能是这单元难度降低的原因,但也因为自己的不仔细阅读jml规格出现了不应该的bug。
通过JML单元作业的学习,我也认识到了JML这个契约的好处,它相当于了使用者和生产商的一个说明书,必须遵守说明书上的规定,才会有正确的结果,同时也只会出现规定的异常情况,使用者无需关注实现的细节,而且可以基于这种规范的语言自动生成测试集进行覆盖下测试,提高了代码的准确性,除此之外,也便于同行者阅读和了解代码的功能,这对一个工程是十分有利。
最后我学会了使用junit进行单元测试,同时也复习和学习了一些优秀的大师算法。总而言之,了解了一个全新的领域上的内容,还是受益匪浅滴。希望下个单元继续加油!