BUAA_OO_UNIT_3
JML语言及工具
JML语言理论
JML语言利用前置条件、后置条件、不变式等约束语法,描述了Java程序的数据、方法、类的规格,是一种契约式程序设计的实现工具。
1. 常用的JML语言特性
-
esult:表示方法的返回值
-
old(expr):表示在方法执行前的值‘
-
forall:全称量词修饰的布尔表达式,可声明局部变量、覆盖变量的取值范围
-
exists:存在量词修饰的布尔表达式
-
sum, max, min:给定范围的表达式进行运算
-
<==>, <==, ==>:逻辑推理表达式
2. 数据规格
- 不变式invariant:在成员处于可见状态下必须满足的特性。其中可见状态可理解为完整的稳定状态。
- 修改约束constraint: 描述前序可见状态 —> 当前可见状态的变化约束。
3. 方法规格
-
前置条件 requires
-
后置条件ensures
-
副作用影响要求assignable modifiable
-
pure方法
-
方法的异常行为normal_behavior, also, exceptional_behavior, signals () expr, signals_only
-
方法规格的原则
- 关注执行效果和造成的其他影响
- 无需关注方法本身的实现方法
- 本质上是数据约束
- requires语句需要覆盖所有可能的情况,包括exceptional_behavior和normal_behavior
- 条件互斥,并集为全集
4. 类规格
在类内利用一些规格变量对类的数据结构维护进行抽象描述,同样的与具体容器、对象等无关。
JML应用工具链
- OpenJML,可以在eclipse尝试,要求必须jdk版本为1.8
- JMLUnitNG
- 可以生成测试样例,进行自动化测试
- 使用方法为命令行
部署JMLUnitNG/JMLUnit,针对Group接口的实现自动生成测试用例
- 首先要下载JMLUnit
- 之后用命令行跑JMLUnit,测试文件仅留下MyGroup,Group和Person
- 效果图如下
之后运行MyGroup_JML_Test,即可自动测试MyGroup
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <<caster.MyGroup@768debd>>.addPerson(null)
Failed: <<caster.MyGroup@490d6c15>>.addPerson(null)
Failed: <<caster.MyGroup@449b2d27>>.addPerson(null)
Failed: <<caster.MyGroup@5479e3f>>.delPerson(null)
Failed: <<caster.MyGroup@27082746>>.delPerson(null)
Failed: <<caster.MyGroup@66133adc>>.delPerson(null)
Passed: <<caster.MyGroup@7bfcd12c>>.equals(null)
Passed: <<caster.MyGroup@42f30e0a>>.equals(null)
Passed: <<caster.MyGroup@24273305>>.equals(null)
Passed: <<caster.MyGroup@5b1d2887>>.equals(java.lang.Object@46f5f779)
Passed: <<caster.MyGroup@1c2c22f3>>.equals(java.lang.Object@18e8568)
Passed: <<caster.MyGroup@33e5ccce>>.equals(java.lang.Object@5a42bbf4)
Passed: <<caster.MyGroup@270421f5>>.getAgeMean()
Passed: <<caster.MyGroup@52d455b8>>.getAgeMean()
Passed: <<caster.MyGroup@18ef96>>.getAgeMean()
Passed: <<caster.MyGroup@6956de9>>.getAgeVar()
Passed: <<caster.MyGroup@769c9116>>.getAgeVar()
Passed: <<caster.MyGroup@6aceb1a5>>.getAgeVar()
Passed: <<caster.MyGroup@2d6d8735>>.getConflictSum()
Passed: <<caster.MyGroup@ba4d54>>.getConflictSum()
Passed: <<caster.MyGroup@12bc6874>>.getConflictSum()
Passed: <<caster.MyGroup@de0a01f>>.getId()
Passed: <<caster.MyGroup@4c75cab9>>.getId()
Passed: <<caster.MyGroup@1ef7fe8e>>.getId()
Passed: <<caster.MyGroup@67117f44>>.getRelationSum()
Passed: <<caster.MyGroup@5d3411d>>.getRelationSum()
Passed: <<caster.MyGroup@2471cca7>>.getRelationSum()
Passed: <<caster.MyGroup@5fe5c6f>>.getSize()
Passed: <<caster.MyGroup@6979e8cb>>.getSize()
Passed: <<caster.MyGroup@763d9750>>.getSize()
Passed: <<caster.MyGroup@5c0369c4>>.getValueSum()
Passed: <<caster.MyGroup@2be94b0f>>.getValueSum()
Passed: <<caster.MyGroup@d70c109>>.getValueSum()
Failed: <<caster.MyGroup@17ed40e0>>.hasPerson(null)
Failed: <<caster.MyGroup@50675690>>.hasPerson(null)
Failed: <<caster.MyGroup@31b7dea0>>.hasPerson(null)
Passed: <<caster.MyGroup@3ac42916>>.sumChange(-2147483648, -2147483648)
Passed: <<caster.MyGroup@47d384ee>>.sumChange(-2147483648, -2147483648)
Passed: <<caster.MyGroup@2d6a9952>>.sumChange(-2147483648, -2147483648)
Passed: <<caster.MyGroup@22a71081>>.sumChange(0, -2147483648)
Passed: <<caster.MyGroup@3930015a>>.sumChange(0, -2147483648)
Passed: <<caster.MyGroup@629f0666>>.sumChange(0, -2147483648)
Passed: <<caster.MyGroup@1bc6a36e>>.sumChange(2147483647, -2147483648)
Passed: <<caster.MyGroup@1ff8b8f>>.sumChange(2147483647, -2147483648)
Passed: <<caster.MyGroup@387c703b>>.sumChange(2147483647, -2147483648)
Passed: <<caster.MyGroup@224aed64>>.sumChange(-2147483648, 0)
Passed: <<caster.MyGroup@c39f790>>.sumChange(-2147483648, 0)
Passed: <<caster.MyGroup@71e7a66b>>.sumChange(-2147483648, 0)
Passed: <<caster.MyGroup@2ac1fdc4>>.sumChange(0, 0)
Passed: <<caster.MyGroup@5f150435>>.sumChange(0, 0)
Passed: <<caster.MyGroup@1c53fd30>>.sumChange(0, 0)
Passed: <<caster.MyGroup@75412c2f>>.sumChange(2147483647, 0)
Passed: <<caster.MyGroup@282ba1e>>.sumChange(2147483647, 0)
Passed: <<caster.MyGroup@13b6d03>>.sumChange(2147483647, 0)
Passed: <<caster.MyGroup@f5f2bb7>>.sumChange(-2147483648, 2147483647)
Passed: <<caster.MyGroup@73035e27>>.sumChange(-2147483648, 2147483647)
Passed: <<caster.MyGroup@64c64813>>.sumChange(-2147483648, 2147483647)
Passed: <<caster.MyGroup@3ecf72fd>>.sumChange(0, 2147483647)
Passed: <<caster.MyGroup@483bf400>>.sumChange(0, 2147483647)
Passed: <<caster.MyGroup@21a06946>>.sumChange(0, 2147483647)
Passed: <<caster.MyGroup@77f03bb1>>.sumChange(2147483647, 2147483647)
Passed: <<caster.MyGroup@326de728>>.sumChange(2147483647, 2147483647)
Passed: <<caster.MyGroup@25618e91>>.sumChange(2147483647, 2147483647)
===============================================
Command line suite
Total tests run: 67, Failures: 10, Skips: 0
===============================================
测试分析
可以看到,对输入为int类型的方法,主要是对边缘数据,和0进行了大量反复的测试,对于传入对象的方法,传入了null来进行测试,所以JMLUnit主要测试的是边缘条件,但是缺乏普适性,所以不能单纯的以JMLUnit的结果为标准。
作业架构分析
这次的作业递进关系非常明确,我在第一次作业中,采取的均是可维护的思想,在每次对数据进行改变的时候,尽可能的去维护结果,这样在查询的时候就可以非常快速地完成。
第一次作业
第一次作业其实主要的判断就在于连通性的判断,为了实现维护连通性的效果,我利用了传递性,利用了一个结构
每个Set中存储的都是和该Person形成isCircle关系的人,那么要怎么维护呢
我们可以分析一下,怎样算isCircle呢?当两者连通,既然我们基于维护这个思想,那么最好用的思想是什么,是数学归纳法!下面我们开始规划,设n为人数,E为关系数(ar次数):
-
当n = 1,E = 0时,(其实就是第一次ap之后),所有与之联通的人只有自己,当然是成立了
-
假设当n = k, E = m时成立,即该Person对应的Set中存储了所有与该Person形成了isCircle关系的人
-
当n = k + 1, E = m时,就是ap之后,显然成立
-
当n = k, E = m + 1时,即在该人数范围内进行了一次ar操作,我们需要做什么处理呢,如果ar的两个人本来就有isCircle关系,那么显然我们是在一个连通块内进行了一次加关系操作,所以不需要处理;那么如果两个人不在连通块内,那么显然这次ar操作让两个原本独立的连通块形成了一个大的连通块,所以我们要做的就是合并这两个连通块了
经过上述的分析,我们发现了关键在于合并连通块!那么要怎么合并连通块呢?
我采取的方法是共享队列,也就是说在同一个块内的人共享同一个Set,这样合并的话就方便了很多!
举个例子来看看发生了什么,我们现在有两个连通块,连通块A:p1, p2, p3 连通块B: p4, p5, p6
如果ar p1, p4,经过分析我们可以发现需要合并连通块A和连通块B,我们怎么做呢?,连通块A.addall(连通块B)
,对于连通块B中的所有人更新所属队列,即更新为连通块A,这样就可以发现现在p1~p6所有人所属的队列均为连通块A
第二次作业
第二次作业相较于第一次作业,难度没有什么巨大的提升,在第一次快速实现之后(发现仅仅通过给出的接口几乎不能能实现维护),发现了两个函数的复杂度为o(n^2),这种复杂度几乎是不能接受的,所以在对接口的实现过程中,我破坏了接口的结构,完成了维护
第三次作业
其实三次作业的实现都完全没有问题,最主要的事情就是优化优化和优化
最先引起我浓烈的兴趣的时queryBlockSum这个方法,这个方法的JML描述如下
一开始我觉得结果是与people的具体实现相关的,经过和其他人的讨论,首先是证明出了,结果与people的具体实现(具体来说是指people内元素的排列数序)无关,之后用离散数学的知识分析发现每个连通块的人只有在第一次出现的时候才会被录入进sum,所以结果刚好就是blockSum,发现这个之后也是非常惊喜,由于我采用的架构,使我的blockSum可以实现$$o(1)$$复杂度的维护。
插一句,也是这里才让我体会到JML语言的妙处,因为JML无关于具体实现,所以可以采取更少的语句来描述一个需要实现的功能,而不在乎其复杂度,这也要求实现的人绝对不能照搬需求分析。否则JML将会是累赘,是代码的复述。
之后还出现了点双连通分量,这里夹带私货https://blog.csdn.net/weixin_44689094/article/details/106132378
出现了最短路径
这里其实更是体现了写代码到底是在干什么,你给我以需求,我还你于实现,而这之中需要做的就是用算法,用架构串联实现需求。所以代码和算法一定是密不可分的
代码实现的bug和修复
个人情况
在三次强测中均未出现错误,在三次互测中,未被Hack。
Hack情况
在第10和第11次作业中,采用自动化测试的方法均hack到多人bug
如果想定性的看时间,推荐
time java -jar "needToTest.jar" < testData > output
基本出现的bug分为两个类型
- TLE
- 想要维护但是维护的过程中细节出错
心得体会
规格的撰写完全不用在意具体实现,用最稳妥地方式去完成实现,用最精确的范围去框定,用最强迫症的方式去限定细节,我觉得这就是规格撰写的目标。
而实现的过程则就要求考虑实现的效能,算法永远和实现密不可分,就算我们这个单元重点在于JML的理解和运用,也不能o(n^{2}), o(n^{3}) ,o(n!)就那么随意的放上去,可维护,低复杂度永远是需要注意和追求的!!
在这个单元的学习中,我第一次,真的是第一次,写这么框架清晰,接口设计完整的代码,这次轻松实现更是让我体会到了架构的重要性,理清了架构,具体实现绝不是问题(如果不需要自己造轮子的话),而第一次作业中采纳了传递性的结构也让我三次作业每次仅需注意新功能,考虑了之后的需求,这才能让代码的可迭代行提升,总体来说,这单元的体验还是不错的
但是我的一些疑问就是JML这个东西实在是,想要使用工具链,强迫我从jdk13换回了jak8,安装了eclipse,各种尝试,我的一些期望就是能够采取一些工具链更好获取,更加现代化的工具。