本单元的三次作为均为根据jml规格完成代码。三次作业逐渐丰富了Person类、Network类和Group类,模拟了一个较为复杂的人际关系网络;通过Network类里面的方法进行人与人、群体与群体之间的一些交互,主要实现了一些与图有关的算法,给我了一种补习数据结构课的感觉。
本文将从JML语言梳理、部署SMT Solver进行验证、部署JML UnitNG并针对Group接口的实现进行测试、架构设计梳理以及分析代码实现的bug和修复情况方面对第三单元的三次作业进行总结,并在最后阐述对规格撰写和理解上的心得体会。
BUAA_OO 第三单元JML规格作业总结JML语言梳理JML语言JML相关语法(1)表达式(2)方法规格应用工具链部署SMT Solver进行验证Type-checkingStatic checkingRuntime assertion checking部署JML UnitNG并针对Group接口的实现进行测试架构设计梳理(1)第九次作业(2)第十次作业(3)第十一次作业分析代码实现的bug和修复情况第九次作业第十次作业第十一次作业心得体会
JML语言梳理
JML语言
-
概述: JML是一种形式化的、面向Java的行为接口规格语言(Behavior Interface Specification Language,BISL),使用javadoc注释的方式来表示规格 。
-
编译:规格是用Java注释的方式编写的,因此可以被任意Java编译器编译。
-
思想:JML继承了Eiffel,Larch和Refinement Calculus的思想,其目标是在保持可读性的同时,提供严格的形式语义。
-
作用:JML提供语义来严格描述Java模块的行为,防止模块设计者的设计错误。JML使用各种验证工具,例如运行时断言检查器和扩展静态检查器(ESC / Java),可帮助程序员进行开发。
JML相关语法
以下内容参考了http://www.eecs.ucf.edu/~leavens/JML//prelimdesign/prelimdesign.html,以及下发的PDF资料。
(1)表达式
JML的表达式与Java表达式有较大的差异,JML无法使用Java中的赋值语句、自增语句等,也无法调用一个不为pure的方法。
-
\forall和\exists表示全称和特称量词。
-
\max、 \min、\product和\sum分别表示最大值、最小值、连乘结果和连加结果。
-
\result:表示一个非void类型的方法执行所获得的结果。
\old(expr):用来表示一个表达式expr在方法执行前的取值。
-
==> 、<==、<==> 和 <=!=>用于表示推导关系,含义与离散数学中的含义类似。
(2)方法规格
-
requires:声明方法的前置条件。
ensures:说明方法的后置条件。
assignable:进行副作用范围限定,常搭配\nothing和\everthing,与之相反的是\not_assigned。
-
normal_behavior:表示正常功能行为。
exceptional_behavior:表示异常行为。
signals:表示抛出的异常和抛出条件。
应用工具链
目前, 许多基于 JML的验证、调试和测试工具已经非常成熟, 例如运行时刻的断言检查器 (Runti me Assertion Checker)、JmlUnit
、JMLAutoTest
等等。利用这些支撑工具, JML 规范可以被翻译为可识别的程序代码, 并被动态断言检查器检验, 也可以进行测试框架、测试用例等的自动生成。
Openjml可以对生成的类文件进行JML规范检查。Openjml使用SMT Solver来对检查程序实现是否满足所设计的规格(specification)。
JMLUnitNG/JMLUnit可以实现自动生成测试用例,本次主要使用了JMLUnitNG对代码进行自动化测试。
JMLdoc工具可在生成的HTML格式文档中包含JML规范,本次作业我没有用过这一工具。
部署SMT Solver进行验证
按照 http://www.openjml.org/documentation/execution.shtml 给出的方法对本次作业的Person
类进行了三项检查如下所示:
Type-checking
类型检查只能确保jml
注释的格式正确,但是对规格内容不进行检查。官方下发的jml没有类型问题。
java -jar openjml.jar Person.java
-
并没有什么输出
Static checking
java -jar openjml.jar -esc Person.java
-
有很多行输出
Person.java:59: 警告: The prover cannot establish an assertion (Postcondition: Person.java:52: 注: ) in method equals
return true;
^
Person.java:52: 警告: Associated declaration: Person.java:59: 注:
@ ensures \result == false;
^
Person.java:56: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: openjml.jar(specs/java/lang/Object.jml):76: 注: ) in method equals
return ((Person) obj).getId() == id;
^
openjml.jar(specs/java/lang/Object.jml):76: 警告: Associated declaration: Person.java:56: 注:
@ public normal_behavior
^
Person.java:56: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: Person.java:44: 注: ) in method equals
return ((Person) obj).getId() == id;
^
Person.java:44: 警告: Associated declaration: Person.java:56: 注:
@ public normal_behavior
^
Person.java:76: 警告: The prover cannot establish an assertion (ExceptionalPostcondition: Person.java:62: 注: ) in method isLinked
if (person.getId() == item.getId()) {
^
Person.java:62: 警告: Associated declaration: Person.java:76: 注:
/*@ public normal_behavior
.....................静态检查好像出现了问题, 指定的
prover
不能建立断言。
Runtime assertion checking
使用-rac
选项可以执行运行时检查。
java -jar openjml.jar -rac Person.java
-
并没有什么输出
部署JML UnitNG并针对Group接口的实现进行测试
尝试采用JML UnitNG对Group接口进行测试,但是经过努力之后没有跑通对Group接口的测试,这里贴出我对Person接口进行的JML UnitNG测试过程。
-
将Person类放到test文件夹内,将Person类的包名改为test
-
java -cp jmlunitng.jar test.Person_JML_Test
生成的数据大多是null或者是int类型的边界收据,感觉对于实际测试的用途不大,因此我本次还是采取了传统的数据生成+对拍进行黑盒测试。
架构设计梳理
(1)第九次作业
第九次作业较为容易,在理解正确ml规格的情况下不容易出错,在运行时间上也没有很高的要求。在设计时,为了提高查找的效率,我使用了Hashmap
作为容器,将每个人独有的id
作为key
去映射到每一个myPerson
对象,在查找时只有O(1)
的复杂度,相较于数组等方式具有非常大的优势,该设计思路也应用在第二次作业和第三次作业的拓展中。
在本次作业中最为复杂的iscircle()
方法中,我使用了BFS
广度优先遍历,而没有使用并查集算法,每次查询只需要判断从一个顶点能否搜索到另一个顶点即可,在时间复杂度上也可以接受。
(2)第十次作业
第十次作业增加了Group类
,Group类
基本上是一些查找和查询的操作。
由于Group
中人数较多,查找信息的时候如果每次都要遍历所有Person会有O(n)或O(n^2)
的复杂度,容易导致超时,因此需要将这些信息以变量的形式缓存在每个Group之中,在调用查询的方法时经过简单的运算直接返回对应的变量即可。在Group中我设置了如下的变量:relationSum
、valueSum
、conflictSum
、ttage
和tt2age
,并在每次对Group
进行addPerson
操作时,容易想到维护上面提到的变量;但是也不能忽略addRelation
时,两个没有关系的人加入了关系,对于relationSum
、valueSum
都有影响。查找平均数和方差的操作复杂度是O(n)
,在这次作业的情况下不会出现超时,但是通过维护年龄之和、年龄平方之和两个变量可以避免一次次的O(n)
查询。另外在计算方差的时候需要考虑无法整除带来的精度问题,即不可以直接使用概率统计给出的方差公式(例如2、3、5三个数就会造成精度的问题),只能在一定范围内进行化简。
(3)第十一次作业
由于作业具有增量拓展的特性,因此这里只贴出了第十一作业的UML类图。
第十一次作业涉及的算法知识比较多,如果选取不合适的算法极易造成CTLE的出现,因此需要谨慎的选择算法并且做好充足的测试,可以粗略计算使用的方法在课程组所给出的数据范围内会不会出现问题。
在查询Group内的信息时我延用了第十次作业的方法,在新增和删去Person的时候维护相应的变量。在维护relationSum
和valueSum
的时候需要遍历Group
中的Person
,删除可以仿照新增操作去写。需要特别注意插入删除操作和遍历操作的先后顺序,删除成员应该在删除之后在进行遍历,减一的操作。
查询最短路径时我使用了堆优化的Dijkstra算法,维护一个有序的优先队列,使用的是JAVA中的PriorityQueue
,通过重写compare
方法来保证找到每次的最短路径,时间复杂度为O(n*log(V))。修改了我之前书写的iscircle
方法,为了queryBlockSum()
重写了并查集算法,只需要遍历Network
中的人,计算出集合的总数即可。
查询强连通queryStrongLinked(int id1, int id2)
的方法比较复杂,我使用了枚举每个点进行删除尝试,之后利用BFS
判断在删除一个点后id1和id2是否仍然相连的方法。这种方法的原理是图论中的Menger定理,这里引用了讨论区某位同学的讲解:设x和y为图G中两个不相邻的顶点,则G中内部不相交的(X,Y)路的最大数目=G中最小的xy顶点分隔集的顶点数。
若不相交的(x,y)路的最大数目为2,即x与y强连通,那么最小的xy顶点分隔集的顶点数为2,也就是说只有在删去两个点的情况下才能破坏x和y的连通性。因此,在只删去任意一个点的情况下,两个强连通的点仍然能够保持连通状态。
这种方法值得注意的是在查询的两个节点直接相连时,需要通过一些方式去掉两点之间的关系,在通过1次BFS找到两点之间有没有其他的路径。这样做的理由时,如果两个点直接相连,那么无论去除哪一个点,都不会改变两点之间的连通性,所以需要进行这样的特判处理。
-
UML类图:
Network
类中比较复杂的算法BFS
,并查集,迪杰斯特拉
都通过内部类的方式加以实现,这样做的好处是无须在u、Network
中声明一些无关的全局变量,另外相比外部的算法类,内部类可以直接访问到类Network
中的属性,无需进行传递参数等一系列复杂的操作。
-
Metrics:可以看到复杂度主要集中在一些算法里面,为了方法的完整性我没有对他们进行特意的解耦操作。
Method ev(G) iv(G) v(G) MainClass.main(String[]) 1 1 1 MyGroup.MyGroup(int) 1 1 1 MyGroup.addPerson(Person) 1 3 3 MyGroup.addRelatonEffect(Person,Person) 1 3 3 MyGroup.delPerson(Person) 1 3 3 MyGroup.equals(Object) 3 2 4 MyGroup.getAgeMean() 2 1 2 MyGroup.getAgeVar() 2 1 2 MyGroup.getConflictSum() 1 1 1 MyGroup.getId() 1 1 1 MyGroup.getPeopleSize() 1 1 1 MyGroup.getRelationSum() 1 1 1 MyGroup.getValueSum() 1 1 1 MyGroup.hasPerson(Person) 1 1 1 MyNetwork.BfsPath.BfsPath(int,int) 1 4 4 MyNetwork.BfsPath.bfs() 5 3 5 MyNetwork.BfsPath.getResult() 5 3 6 MyNetwork.DsUnion.DsUnion() 1 1 1 MyNetwork.DsUnion.find(int) 2 1 2 MyNetwork.DsUnion.merge(int,int) 1 2 2 MyNetwork.DsUnion.putEle(int) 1 1 1 MyNetwork.MyNetwork() 1 1 1 MyNetwork.addGroup(Group) 2 1 2 MyNetwork.addPerson(Person) 2 1 2 MyNetwork.addRelation(int,int,int) 4 3 6 MyNetwork.addtoGroup(int,int) 5 1 5 MyNetwork.borrowFrom(int,int,int) 3 2 4 MyNetwork.compareAge(int,int) 2 2 3 MyNetwork.compareName(int,int) 2 2 3 MyNetwork.contains(int) 1 1 1 MyNetwork.delFromGroup(int,int) 4 1 4 MyNetwork.getGroup(int) 1 1 1 MyNetwork.getPerson(int) 2 2 2 MyNetwork.isCircle(int,int) 3 2 4 MyNetwork.queryAcquaintanceSum(int) 2 1 2 MyNetwork.queryAgeSum(int,int) 1 2 4 MyNetwork.queryBlockSum() 1 2 2 MyNetwork.queryConflict(int,int) 2 2 3 MyNetwork.queryGroupAgeMean(int) 2 1 2 MyNetwork.queryGroupAgeVar(int) 2 1 2 MyNetwork.queryGroupConflictSum(int) 2 1 2 MyNetwork.queryGroupPeopleSum(int) 2 1 2 MyNetwork.queryGroupRelationSum(int) 2 1 2 MyNetwork.queryGroupSum() 1 1 1 MyNetwork.queryGroupValueSum(int) 2 1 2 MyNetwork.queryMinPath(int,int) 7 7 11 MyNetwork.queryMoney(int) 2 1 2 MyNetwork.queryNameRank(int) 2 2 4 MyNetwork.queryPeopleSum() 1 1 1 MyNetwork.queryStrongLinked(int,int) 4 2 5 MyNetwork.queryValue(int,int) 3 2 4 MyPerson.MyPerson(int,String,BigInteger,int) 1 1 1 MyPerson.addAcquaintanceWithValue(Person,int) 1 1 1 MyPerson.compareTo(Person) 1 1 1 MyPerson.equals(Object) 3 2 4 MyPerson.getAcquaintance() 1 1 1 MyPerson.getAcquaintanceSum() 1 1 1 MyPerson.getAge() 1 1 1 MyPerson.getCharacter() 1 1 1 MyPerson.getId() 1 1 1 MyPerson.getName() 1 1 1 MyPerson.isLinked(Person) 2 2 3 MyPerson.queryValue(Person) 2 2 2 com.oocourse.spec3.exceptions.EqualGroupIdException.EqualGroupIdException() 1 1 1 com.oocourse.spec3.exceptions.EqualGroupIdException.print() 1 1 1 com.oocourse.spec3.exceptions.EqualPersonIdException.EqualPersonIdException() 1 1 1 com.oocourse.spec3.exceptions.EqualPersonIdException.print() 1 1 1 com.oocourse.spec3.exceptions.EqualRelationException.EqualRelationException() 1 1 1 com.oocourse.spec3.exceptions.EqualRelationException.print() 1 1 1 com.oocourse.spec3.exceptions.GroupIdNotFoundException.GroupIdNotFoundException() 1 1 1 com.oocourse.spec3.exceptions.GroupIdNotFoundException.print() 1 1 1 com.oocourse.spec3.exceptions.PersonIdNotFoundException.PersonIdNotFoundException() 1 1 1 com.oocourse.spec3.exceptions.PersonIdNotFoundException.print() 1 1 1 com.oocourse.spec3.exceptions.RelationNotFoundException.RelationNotFoundException() 1 1 1 com.oocourse.spec3.exceptions.RelationNotFoundException.print() 1 1 1 com.oocourse.spec3.main.Runner.Runner(Class extends Person>,Class extends Network>,Class extends Group>) 1 1 1 com.oocourse.spec3.main.Runner.addGroup() 1 2 2 com.oocourse.spec3.main.Runner.addPerson() 1 2 2 com.oocourse.spec3.main.Runner.addRelation() 1 3 3 com.oocourse.spec3.main.Runner.addtoGroup() 1 4 4 com.oocourse.spec3.main.Runner.borrowFrom() 1 3 3 com.oocourse.spec3.main.Runner.compareAge() 1 2 4 com.oocourse.spec3.main.Runner.compareName() 1 2 4 com.oocourse.spec3.main.Runner.delFromGroup() 1 4 4 com.oocourse.spec3.main.Runner.queryAcquaintanceSum() 1 2 2 com.oocourse.spec3.main.Runner.queryAgeSum() 1 1 1 com.oocourse.spec3.main.Runner.queryBlockSum() 1 1 1 com.oocourse.spec3.main.Runner.queryCircle() 1 3 3 com.oocourse.spec3.main.Runner.queryConflict() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupAgeMean() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupAgeVar() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupConflictSum() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupPeopleSum() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupRelationSum() 1 2 2 com.oocourse.spec3.main.Runner.queryGroupSum() 1 1 1 com.oocourse.spec3.main.Runner.queryGroupValueSum() 1 2 2 com.oocourse.spec3.main.Runner.queryMinPath() 1 2 2 com.oocourse.spec3.main.Runner.queryNameRank() 1 2 2 com.oocourse.spec3.main.Runner.queryPeopleSum() 1 1 1 com.oocourse.spec3.main.Runner.queryStrongLinked() 1 2 2 com.oocourse.spec3.main.Runner.queryValue() 1 3 3 com.oocourse.spec3.main.Runner.query_money() 1 2 2 com.oocourse.spec3.main.Runner.run() 1 28 28
分析代码实现的bug和修复情况
第九次作业
公测、互测:什么都没有发生
第十次作业
公测:什么都没有发生
互测:同屋一位同学在计算relationSum
和valueSum
使用了双重循环的方法,这种O(n^2)
的算法很容易被卡,我构造了特殊数据使其程序出现TLE。
第十一次作业
公测:我在公测中出现了两个bug,一是在计算年龄方差时,由于用于储存年龄平方之和的数据会产生溢出,因此我将这个变量类型定义为long,但在long与int进行混合运算时我没有进行强制类型转换(现在也不是很理解这一神秘的错误,不过警示我不能随便的进行强转操作),导致错误。修复时我在对每个int变量都进行了手动的强制类型转换,程序变为正常。 二是两个集中测试qmp的数据点出现超时,经过仔细地思考我发现如果要查询地两个点如果不相连,我的程序也会进行一整套的迪杰斯特拉,这是十分浪费时间的。我通过在执行前先利用高效的并查集算法判断是否直接相连,成功的将时间控制到了2s以内。
互测:我观察到很多同学的迪杰斯特拉算法都没有进行堆优化,甚至有使用DFS算法的情况,我利用这一点构造出了大量的qmp查询,成功hack到几位同学。同屋的一位同学的qsl貌似有错误,可能是没有注意到我上面分析时提到的一些细节,我针对性的hack了几发。
心得体会
第三单元的主要内容是阅读JML规格并实现将规格进行具体实现,如果单纯的按照规格给出的方法去实现,那么超时是必然的。所以我们在实现时需要考虑算法问题,同时还需要考虑架构问题,以及一些细节问题如容器的使用等问题。