OO第三单元JML规格总结
1.JML理论基础和JML工具链
1.1JML理论基础
JML简介
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。
原子表达式
\result表达式:表示一个非void类型的方法执行所获得的结果,即方法执行后的返回值。
eg:\result = people.size(),表示该方法返回的结果是people容器的大小。
\old(expr)表达式:用来表示一个表达式expr在相应方法执行前的取值。
eg:people.size() = \old(people.size()),表示方法执行前后people容器的大小不变
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true,否则返回false。
\not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似。
\nonnullelements(container)表达式:表示container对象中存储的对象不会有null。
\type(type)表达式:返回类型type对应的类型(Class),如type(boolean)为Boolean.TYPE。
\typeof(expr)表达式:该表达式返回expr对应的准确类型。如\typeof(false)为Boolean.TYPE。
量化表达式
\forall表达式:类似于for循环遍历,表示对于给定范围内的元素,每个元素都满足相应的约束。如果都满足返回true,否则返回false。特别注意,当给定的范围中无元素满足要求时返回true。
eg:(\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j]),意思是针对任意0<=i eg:(\forall int i; 0 < i <1; a[i] < 1),该表达式返回true。 \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。当存在一个元素满足时返回true,若全部元素都不满足则返回false。 eg:(\exists int i; 0 <= i && i < 10; a[i] < 0),表示针对0<=i<10,至少存在一个a[i]<0。 \sum表达式:返回给定范围内的表达式的和。 eg:(\sum int i; 0 <= i && i < 5; i),这个表达式的意思计算[0,5)范围内的整数i的和,即0+1+2+3+4==10。 \product表达式:返回给定范围内的表达式的连乘结果。 eg:(\product int i; 0 < i && i < 5; i),这个表达式的意思是针对(0,5)范围的整数的连乘结果,即1* 2* 3 * 4。 \max表达式:返回给定范围内的表达式的最大值。 eg:(\max int i; 0 <= i && i < 5; i),这个表达式返回[0,5)中的最大的整数,即4。 \min表达式:返回给定范围内的表达式的最小值。 eg:(\min int i; 0 <= i && i < 5; i),这个表达式返回[0,5)中的最小的整数,即0。 \num_of表达式:返回指定变量中满足相应条件的取值个数。 eg:(\num_of int x; 0 可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。 eg:new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() } 子类型关系操作符:E1<:E2,如果E1是E2的子类或者同类,则返回true,否则返回false。 等价关系操作符:b_expr1<==>b_expr2(等价,相当于==)或者b_expr1<=!=>b_expr2(不等,相当于!=)。 推理操作符:b_expr1==>b_expr2或者b_expr2<==b_expr1。类似于离散中的蕴含式。 变量引用操作符(常用):\nothing指示一个空集,\everything指示一个全集。 前置条件(pre-condition):前置条件通过requires子句来表示:requires P;。方法调用者需要确保满足的条件。 后置条件(post-condition):后置条件通过ensures子句来表示:ensures P;。在满足require后该方法需要确保返回结果满足的条件。 副作用范围限定(side-effects):使用关键词assignable或者modifiable。表示在方法调用后可能对某些变量进行修改。 虽然有require的存在,但我们依然不能保障方法调用者在调用该方法时满足了我们给出的条件。所以通常一个方法会有正常功能行为和异常行为两种行为。在JML中我们用public normal_behavior表示接下来是对正常行为的规格描述,用public exceptional_behavior表示接下来是对异常行为的规格描述。在描述异常行为时,后置条件常常表示为抛出异常,使用signals(后面可以有多个异常)或signals_only(后面只能由一个异常)子句来表示。 eg:signals (***Exception e) b_expr。意思是当b_expr为true时,方法会抛出括号中给出的相应异常e。 不变式invariant:不变式是要求在所有可见状态下都必须满足的特性。 eg:invariant counter >= 0。表示counter需要总是满足大于等于0的条件。 状态变化约束constraint:对象的状态在变化需要满足的约束。 eg:constraint counter == \old(counter)+1。表示counter每次修改只能+1。 下午按照大佬们的教程先是花了近一个小时下载下来,然后捣鼓了半天还是一堆报错,只能放弃。不过看博客园很多大佬们的测试结果,JMLUnitNG只是毫无意义的对各个方法进行边界测试,然后返回结果。我觉得这似乎对于测试自己的程序并没有什么帮助,希望在以后他的功能能够更加完善吧,最重要的是更加方便使用吧。 第一次作业的逻辑相对简单,绝大部分的方法是完全按照规格填写的。其中相对复杂的方法是iscircle方法,在完成这个方法时我采用了dfs算法,并在MyNetwor类中添加了dfs方法然后在iscircle中进行调用,在完成这个dfs时我为每个Person新增加了一个dfsMark的属性,用来标记他们有没有被访问过,这种做法的好处是在每次dfs时不用创建新数组来作为标记数组。 比较不幸的是由于数据结构的基础薄弱,在完成dfs方法时参照了网上求解全排列的dfs代码模板,然后在自己测试的时候也由于数据量过小并没有发现问题,最后在强测时TLE了十几个点。出现这个bug的根本原因是自己对于算法的代码理解不透彻,一味的照搬了网上的代码,而网上这个写法的问题在于求解全排列的dfs是遍历了所有可能的遍历情况(这个复杂度应该是很恐怖的,按照我的理解复杂度应该是O((n+e)*n!)即一次dfs的复杂度O(n+e)*全排列的个数n!,所以真的是想不TLE都难),而iscircle方法的本意是只需要遍历一种情况,所以最后会TLE。还有一个非常不小心的BUG由于对于规格的阅读不仔细造成的,在addrelation方法中有一种normal_behavior是require id1 = id2 && contains(id1);assignable /nothing,按照规格的意思应该在满足条件后直接return,什么都不做。结果我在实现时就没有判断这种情况(以为这样是正确的),导致了如果出现这种情况则会抛出异常,这个错误又构成了我强测中的好几个wrong answer。这两个致命的错误导致了我的强测只有5分,也没能进入互测。 第二次作业新增了Group类和一些相关的方法,在相关方法的实现上也是完全照搬了规格。性能优化方面,在原有的ArrayList存储上增加了Hashmap用于各种查询类的方法,而ArrayList则用于需要遍历计算的方法。 同样的非常不幸,这一次又未能进入互测,其中最致命的原因是在MyNetwork中有方法需要查询特定的group是否存在于groups中,不知为何(一定是脑抽了)为了完成这个查询动作,我竟然自己编写了一个containsGroup方法。而在我的contains方法没有判断特定id对于的group为null的情况,导致在getGroup(id)为null时调用equals方法会抛出空指针异常。这个致命的错误送了我16个RE,直接让我和互测说再见了。在修复这个BUG后还有二个TLE。剩下的二个TLE分别由qgvs,qgam和qgav造成。 首先看qgsvs,这个方法和qgrs一样都是O(n^2)的复杂度。这次强测最多有5000人,即n=5000,指令最多有10w。所以粗略估计最大复杂度大概是O(2.5^12),而强测的时间只有6.66s,所以TLE时板上钉钉的。但是只要加上了记忆化存储就可以很好的解决这个问题,所谓记忆化存储就是将计算结果保存下来,在每一个元素属性发生改变时重新计算,这样每次调用该方法查询时只用O(1)的复杂度,所用的时间也大大减少。 然后是qgav和qgam,简单分析下qgam是O(n)的复杂度,qgav也是O(n)的复杂度,按理来说不应该TLE。但是我在写这个方法的时候十分智障的在qgav的循环中调用了qgam导致了这个方法的复杂度变成了O(n^2),所以自然而然就TLE了。解决办法是在进入循环前就调用qgam,存下查询得到的值。 第三次作业的整体结构和第二次作业差不多,但是新加了许多图的算法相关的方法,也终于将图的特点展现出来了。下面来看几个比较重要的方法。 1.queryMinPath:求最短路。一看到这个就想到了dijkstra算法,这个算法的复杂度是O(n^2),看了讨论区大佬的分析这个复杂度是很容易翻车的,需要使用堆优化的dijkstra复杂度是O(nlogn)。 2.qureyStrongLinked:求点双连通分量,最优解是Tarjan算法,复杂度是O(n)。但是这条指令的上限是10条,所有应该是有暴力解法。但是看讨论区中说,双dfs暴力解法是有bug的(虽然最后还是用了这个bug版本),但也有说还是可以双dfs(思路不一样,但当时没有想通是怎么个双dfs法)。 3.queryBlockSum:求连通块的个数。这个一开始读规格并没有搞懂是在求连通块,在看了灌水群后恍然大悟。然后我使用的是dfs遍历所有点的方法,整体的复杂度应该是O(n+e)。虽然复杂度不高,但依然不是很优的办法,最好的还是并查集,复杂度差不多是O(1)。 最后还有一个很重要的方法iscircle,这个从第一次作业就有的方法其实在第三次作业中暗藏杀机。在前两次的作业中对于算法的优化要求不高,用dfs实现iscircle和用并查集实现差别不大。不过到了第三次作业整体的性能要求比较高,而且在很多比较复杂的方法中使用到了iscircle,所以改用并查集实现iscircle还是很有必要的。 先说比较弱智的BUG。首先是在delFromGroup()中,该方法有三种抛出异常的情况,其中有一种判断Network中有没有对应的人,由于我用了Hashmap 修复了这两个弱智bug并没有使我多通过强测的任何一个点,不过之后所有的wa错误点全都指向了queryStrongLinked()。在完成这个方法的时候看了讨论区的相关讨论,得知这是一个求点双连通分量的方法,并看到了大佬们分享的Tarjan算法,不过由于这周在周六才开始完成这次oo作业,花费了3个多小时依然没有搞明白Tarjan算法后只能放弃,转而选择了讨论区中很早提出的两次bfs/dfs算法,虽然知道这个方法有致命的bug,但迫不得已只能强行用了,最后wa了那么多只能说是自食恶果了。 除开上述bug,最后还有3个TLE。这三个问题应该在于求最短路时没有进行堆优化,iscircle方法也没有使用O(1)的并查集还是用的O(n+e)的dfs。 JML对需要实现的功能要求给出了严谨的形式化描述,这样我们在编写代码时有了极大的便利。但是在写代码时又切忌照搬规格编写,比如第三次作业中的qbs,如果完全按照规格来实现这个方法是根本行不通的。所以在写代码之前我们一定要理解好规格,搞清楚这个方法到底在做什么之后再选择合适的算法来实现。 这个单元我完成的其实特别差,两次未进互测,一次强测35分。得到这样的结果很大程度上是由于自己的态度问题。每一次作业都有很弱智但是十分致命的BUG,这体现了在完成作业时的浮躁心态,没有把认真放在心上,只是想敷衍了事(正好有规格写代码又给人感觉是一件轻松的事情,这也促使了我的懈怠)。其实进入大学很长一段时间我的学习状态都不太好,很多时候都只是为了完成任务而学习,没有把学到知识作为第一目标,而且这样的状态在家中表现的更加明显了。 哎,希望在以后能有一些改变吧。集合表达式
操作符
方法规格
类型规格
1.2JML工具链
OpenJML:用于对JML规格进行检查。
Junit:可用于编写单元测试。
JMLUnitNG:可以自动生成基于JML的测试用例。
2.JMLUnitNG/JMLUnit
3.代码架构、BUG与BUG修复
第一次作业的UML类图
第一次作业的BUG
第二次作业的UML类图
第二次作业的BUG
第三次作业的UML类图
第三次作业的BUG
心得体会
一些反省