JML(Java Modeling Language)
做为一种形式化语言,能够约束Java
代码中类和方法的状态和行为造成规格,经过将一系列具体代码实现抽象成明确的行为接口,能够造成一种契约式编程模式,JML
设计者无需考虑实际的数据结构与算法,能够聚焦于程序的总体逻辑,JML
形式化语言的无二义性能让实现者准确理解接口功能,根据问题须要选择合适的实现方式,同时JML
能够帮助实现者开展代码测试与形式化验证,固然这时就要分析出模型语言映射到具体代码的抽象函数了。java
JML
能够内嵌在.java
内,很方便,以// @
行注释或/* @ ... @ */
块注释,JML
表达式支持Java
表达式做为基础。针对更复杂的逻辑引入了量词表达式,\forall
关键字相似数学中的∀
,用法为\forall v; P(v); Q(v)
,等价于∀v(P(v) → Q(v))
;\exists
关键字相似数学中的∃
,用法为\exists v; P(v); Q(v)
,等价于∃v(P(v) ^ Q(v))
;这是在本单元比较经常使用的,固然还有\max, \sum, \min
等实用的关键字。c++
还可使用特殊操做符==> <==>
表示蕴含与等价关系,使用这种操做符能够加强JML
的可读性,同时还有\nothing, \everything
表示当前做用域下的空集全集算法
本单元咱们聚焦于JML
中的方法规格,方法规格针对类中各类方法(构造、修改、查询、生成)的返回值与行为进行约束,方法规格的三大关键是前置条件、后置条件、反作用范围。shell
前置条件经过对参数施加约束,从而区分不一样的处理操做,大致可分为normal_behavior
,expcetional_behavior
,各有各的前置条件,从而经过JML
能够直观看出什么时候须要对异常输入处理,什么时候是正常输入,固然两者的条件不能有交集。若输入均不知足前置条件,咱们在实现中是没有理由进行处理的,所以方法的调用者为了保证结果的可控必须了解前置条件。对于前置条件,使用requires P;
要求输入知足命题P
编程
后置条件对方法调用以后的对象状态与返回值约束,规定方法行为,使用关键字ensures
规定后置条件,对于查询(query)
方法,特别是只关心返回值不改变数据的方法,能够在访问修饰符后加上/*@pure@*/
标记,对于返回值(\result)
的表示,通常使用ensures P(\result);
windows
固然可能在查询中可能也会修改对象,好比惰式更新,修改方法(modify)
也要修改对象,为了表示出修改与其范围,咱们须要调用前的状态,ensures P
针对的是调用后状态,\old(obj)
关键字就能够看做对象/基本类型在方法调用前的快照。好比对于涉及容器的数据管理类,常用增删Add, Remove
方法,\old
就派上用场了,可使用P(\old(obj)) ==> Q(obj)
描述新的对象状态,固然这对对象的新状态是一种弱约束,好比不能保证Add
后不会加入不指望的元素,能够在另外一方向再加一个条件(constains(obj) && !isforadd(obj)) ==> \old(constains(obj))
进行最小化,可见利用requires, ensures
已经能够基本描述方法了数组
还有一个方面就是反作用范围(side effect)
了,咱们能够列出本方法中可能修改的类中(静态)属性,对调用者而言,能够轻松get到是否会修改本身关心的敏感属性,一般使用assignable(modifiable)
来列出可能修改的属性表assignable v1, v2, ...;
,也可使用JML
中的全集,空集网络
上面提到的expcetional_behavior
是针对Java
的异常特性的,使用signals
关键字规范了异常的抛出时机与类型,用法为signals (<Exception Class> e) P;
,等价于当P == true
时throw
异常,本单元对程序鲁棒性的考量也在此体现数据结构
类型规格就是对类中属性数据的约束,相似Java
,JML
可在变量名前加修饰符,non_null
保证容器内无null
,听从这个规约可有效避免访问空引用,也有static, instance
区分静态属性,固然JML
想访问类中private
属性时,相关属性可加/*@spec_public@*/
,但咱们的实现大可没必要彻底按规格来,规格描述也不应关心具体实现中的数据(这一点是在实验课中体会到的),本单元JML
描述中容器都用数组表示,但咱们只要符合规格的约束条件,能够选择更合适的数据结构。本单元代码中未涉及不变式invariant
、状态变化约束constraint
,这两个是比较强的约束条件,前者规定每次属性修改后的规约,后者表示属性修改相对于修改前状态的规约架构
总之JML
对代码实现者很实用,本单元写代码时官方JML
已经给出了总体的结构与逻辑,JML
中还有很多很实用的用法,值得往后深刻学习一哈
关键字 | 做用 |
---|---|
\not_assigned(x,y,...) | 变量是否在方法执行过程当中未被赋值 |
\not_modified(x,y,...) | 变量在方法执行期间的取值是否未发生变化 |
\type(type) | 返回类型type对应的类型 |
\typeof(expr) | 返回expr对应的准确类型 |
\num_of | 返回变量中知足相应条件的元素个数 |
IDEA
貌似没有插件式的JML
工具,仍是挺惋惜的,JML
官网有相关工具的介绍
OpenJML
可用于JML
的语法检查分析,下载解压到项目文件夹下运行
java -jar openjml.jar -exec Solvers-windows\z3-4.7.1.exe -esc com\oocourse\spec3\exceptions\*.java com\oocourse\spec3\main\*.java
发现提示int
与#INT1
不一样啥的,是JML
中三目运算符的解析好像有点问题,把Group
接口中三目修改为
/*@ ensures (\result == 0 && (people.length == 0) || (people.length != 0) && \result == @ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length)); @*/
JMLUnitNG
可基于JML
对代码实现自动生成测试代码并开展测试,使用java -jar jmlunitng.jar 接口 源码
生成了测试代码
与同窗交流貌似须要在JDK 8
环境下才能运行,在IDEA
中Project Structrue -> Project Settings -> Project language level
设置成8
,运行一下针对Group
接口的测试代码
可见JMLUnitNG
是针对极端数据的,说好也是验证了程序的基本功能,说坏数据覆盖面并不广,对本单元做业的帮助有限。
JMLEclipse
:针对Eclipse
的JML
插件JMLOK
:介绍是说随机数据对JML
实现的测试AspectJML tool, JML4c
:运行时规格验证 本次做业要求是对社交关系网络的模拟,很显然是一种图论模型,Person
接口做结点,Network
做图,Group
做子图,JML
规格将层次描述的很清楚,强制Person
管理边集,层次按图的层次就好,在hw9, 10
我还专门引入了边类MyFriend
,主要考虑到可能迭代会出现Person
的子接口。并且针对hw10
的isCircle
还保留了BFS
和Disjoint Sets
两个版本的,主要考虑到最后可能会加入删边操做,并查集的删边操做至少也得是O(V + E)
了,然而都并无,whatever,本单元这一点至关友好,笔者终于不用重构了
选用数据结构方面,考虑到id
范围,类中容器所有选择以id
为键值的Hashmap<>
,固然用两个Hashmap<>
创建起结点输入id
到数组下标index
的可逆函数 + 静态数组也很不错,为了可维护性选了前者
为何要说"Java性能"
JML
设计者只关心类可见的状态,并不关心具体实现的类内部状态,这也是同窗们发挥的空间,但那种直接对着规格照葫芦画瓢的代码是很危险的,笔者仍然记得hw10
用互测规模数据均可以把Group.getAgeVar
朴素遍历卡到1e9
级复杂度(运行分钟级),震惊了。反正适合实际问题的方法才是最好的!其次,Java
还有c++
这种面向对象语言,但凡涉及到字符串String
,容器(container)
之类的,抽象层次一高,就很难针对问题进行优化,致使执行效率低,反正很慢就是了,并且根据数据本单元T
的还很多,因此提升性能很关键,这一部分围绕几个图架构下的关键矛盾方法开扯
前者查询是否为同一连通块内,后者查询连通块数,最优解为并查集(Union-find Sets)
,具体原理老生常谈了。isCircle()
两次find
操做,同时在图内维护连通块数k
,增长结点addPerson
时k++
,增长边addRelation
时进行merge
操做,若为不一样连通块合并,k--
,在i
条指令下复杂度为O(E + i)
固然hw10
中的并查集有很大的隐患,递归形式的find
操做在退化成单链的图中递归层数为V
,笔者经过压力测试发现四千多层就爆栈了,StackOverFlow
由递归层数,JVM
设置的栈大小以及函数参数,局部变量大小决定,笔者决定稳他一手,将find
路径上的结点暂存一下,找到父节点后统一设置父节点,就不递归了
像开头说的,并查集若是面对删边deleteRelation
很棘手,因为查并集路径压缩信息丢失,不能单纯的改父节点,咱们须要遍历点集找到涉及的两个连通块的点集,再遍历一遍打上标记,删边,对于有标记的点展开BFS
从新记录下父节点,O(V) / O(V + E)
,但若是deleteRelation
有条数限制,那又是另外一种权衡了
Group
用于查询子图信息,为了提升效率维护内部属性,反正一堆东西在addPerson / delPerson
时更新就好,平均值维护年龄和suma
,方差维护年龄平方和suma^2
,套公式(suma^2 + mean^2 * size - 2 * size * suma) / size
特别地,子图内的增长边(ar)
能够绕开Group
,因此ar
的时候得更新一下边的数据,没啥可操做的,但必须咱们对变量的维护这个优化要符合JML
(好比hw11
再维护int suma^2
就溢出了,必定得保证正常状况合法结果),对拍走起
通读一下规格,查询两个结点是否在某个阶数大于2
的环路上,在讨论区大手子指点下能够解读为两个结点是否在某个阶数大于2
的点双连通份量上,学习了一下资料,点双连通份量居然是tarjan
?笔者只用过神奇的塔尖求过有向图的强连通份量啊...而后其实把求有向图的强连通份量的tarjan
的DFS
树的出栈条件改一下就行了:递归遍历子节点后再出栈改为每一个子节点递归后出栈。上张简单图(数字是DFS
顺序,箭头是DFS
大概的方向)
能够看到咱们2
次回溯到1
,根据塔尖的递归更新,2, 3 / 4,5
的low
都会标记成1
正好对应两个份量,但在求强连通份量时要等到1
无子节点时再出栈;改改代码,每次回溯到根节点出栈就ok,固然注意不能出栈根节点,它能够处在多个份量中,而且注意特判阶数,O(V + E)
经典带权最短路,经典Dijkstra
,堆优化后O(Vlog(V))
Java
中想要搞一个小顶堆,PriorityQueue / TreeSet
就够用,二者分别基于静态数组与红黑树,固然注意不能修改堆比较器中涉及的类属性,这将破坏堆维护的性质并致使意料以外的Bug
,只能冗余加或先删除再添加,PriorityQueue
就不要先删再加了,毕竟只能按引用查找遍历到下标再删,O(n)
,红黑树的增删都是O(log(n))
public boolean remove(Object o); private int indexOf(Object o);
为啥笔者仍是TLE
一个点,排查后原来是结点联结边集Hashmap<>(initalSize)
初始化容量太大了,Hashmap
迭代器遍历的核心函数为
final HashMap.Node<K, V> nextNode() {... if ((this.next = (this.current = e).next) == null && (t = HashMap.this.table) != null) while(this.index < t.length && (this.next = t[this.index++]) == null); ...}
可见要一次迭代器遍历要遍历哈希表到最后一个键值对,复杂度是容器的线性,因此说容量不能设太大,或者说这种无序型容器就不适合进行频繁的遍历操做,咱们能够冗余存储一个顺序容器。固然对于qmp, qsl
这种频繁访边点权的操做,创建起结点输入id
到数组下标index
的可逆函数 + 静态数组进行离散化就很香了,再把那个图中的边集直接一给MyNetwork
管理,采用链式前向星,就纯数组,绕太低效的容器和对象,甚至能够搬本身的c
代码了,起飞
queryAgeSum()
单点更新,区间查询,教科书的树状数组,queryNameRank
在查询次数远大于节点数时能够二分下界查找,本次做业应该不太实用
这是一个能够快捷开展针对特定方法测试的框架,很实用,由于本单元咱们要抓住关键矛盾,测试复杂方法就行了,用@Before
标记能够在测试前初始化数据,用@Test
开展一次测试,通常测一个函数就行了,Junit
能够把不一样测试的stdout
分开,关于测试方式,咱们每每要优化代码,因此咱们能够在测试类中模拟这些方法的朴素版,好比Group
的,而后随机构造数据,经过Junit
的断言Assertion
机制来验证一下优化版是否保证了正确性,固然能够调用c
来对拍,Junit
内嵌于Java
至关灵活,还能够用于抛出异常的测试和运行时间的分析,是很是有用的Debug
工具
Java
只能传引用的特性;修复: 冗余加入的时候clone()
一下Group.getAgeVar
没注意到size == 0
的状况,divide by zero
,hw10
强测翻皮水qmp
在设置哈希表容量过大时不要迭代器遍历,TLE
一个点 JML
做为一种形式化语言,表意很准确,我以为结合接口API
文档使用也就更直观了,本单元做业没有涉及不变式跟继承关系下的规格啥的。但其实主要让笔者体会到了规格设计中的契约式编程方法,设计者和实现者分离,咱们在做业中扮演的实现者就是要在JML
规格之下考量可维护性,执行效率,可读性,采用最合适的。这是一种高效的开发模式,对咱们在团队项目、工做中颇有用处