第三单元总结
一、JML语言梳理
JML基础知识
JML语言是一种用于撰写Java程序规格的语言,JML通过大量描述行为的结构,规范了方法与类在实现时对于数据的处理。用JML书写的规格逻辑严谨,消除了用自然语言书写时产生的歧义,这也为开展自动化测试创造了条件。
在我看来,JML主要可以用于以下三个方面:
-
在开展程序设计时,通过撰写规格明确方法需要实现的功能以及在实现过程中必须遵循的规则,便于设计人员明确程序的架构。
-
在实现方法时,针对已经写好的JML规格进行实现,可以无歧义地满足设计人员的要求,同时,由于规格对于需求有了较为明确的规定,在书写时出现bug的可能也降低了一些。在后期优化时,也可以不去考虑原代码的实现逻辑,根据JML设计优化方案。
-
在开展测试时,通过JML对前置条件、后置条件、不变式等的约束开展测试。
1.注释结构
JML写在注释里,如果是方法的规格,写在方法的上方,如果是invariant
或是实例成员等,则在类一开始就要写出。JML有两种注释方法,分别是行注释和块注释。行注释要写成//@ annotation
的样子,块注释写成/* annotation */
的样子
2.常用的表达式
\result: 表示一个非void类型的方法的返回值
\old(expr): 表示expr在方法执行前的取值。不过需要注意的是,\old取到的是对expr的引用,因此要注意类似\old(v).methord()与\old(v.methord())的区别
\forall:全称量词,对给定范围内的所有元素都要满足这一要求
\exists:存在量词,给定范围内至少有一个元素满足要求
\sum: 求和
==> & <== :推理操作符
<==>:等价关系
3. 方法规格
- 前置条件:以
requires P
的形式出现,要求调用者保证P为真。一个方法规格可以有多个requires
语句。 - 后置条件:以
ensures P
的形式出现,要求方法执行完毕后P为真 - 副作用:以
assignable
或modifiable
的形式出现,对于方法执行时的副作用给出限定 - 异常:以
signals
的形式出现,对方法的异常给出限定
4. 类型规格
- 不变式:
invariant
要求所有在可见状态下都满足的条件。 - 状态约束变量:
constraint
要求状态变化过程中满足的条件。
应用工具链
-
编写阶段,可以考虑VS Code的插件jml-vscode,这一插件支持jml语言的高亮,但无法进行自动补全。这里以作业中
Network.addPerson()
的规格为例展示效果: -
OpenJML:用于检验JML规格的正确性,可以对程序进行动态和静态检查。
-
SMT Solver:用于验证代码和规格的等价性
-
JMLUnitNG:根据规格生成测试数据验证代码的正确性。
二、用SMT Solver对方法的验证结果
关于OpenJML的安装与使用,本人参考了这篇文章OpenJML入门
这里报告一个坑点:在使用OpenJML时,如果开门报错NoSuchFieldError
,是因为JDK版本的问题,更换为JDK 8即可正常使用。
实话说在这里真的很崩溃,报错报到怀疑人生。
感觉坑点会在于,如果JML里用数组表示的集合的话,实现的时候用HashMap或是ArrayList就会报错,如图:
于是我魔改了MyPerson.java,让它的实现如下图
import java.math.BigInteger;
import java.util.ArrayList;
public class MyPerson{
public int id;
public String name;
public BigInteger character;
public int age;
public int size = 0;
public MyPerson[] acquaintance = new MyPerson[1000];
public int[] value = new int[1000];
public MyPerson(int id, String name, BigInteger character, int age) {
this.id = id;
this.name = name;
this.character = character;
this.age = age;
}
//@ ensures \result == id;
public /*@pure@*/ int getId() {
return id;
}
//@ ensures \result.equals(name);
public /*@pure@*/ String getName() {
return name;
}
//@ ensures \result.equals(character);
public /*@pure@*/ BigInteger getCharacter() {
return character;
}
//@ ensures \result == age;
public /*@pure@*/ int getAge() {
return age;
}
/*@ public normal_behavior
@ assignable \nothing;
@ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId()) || person.getId() == id;
@*/
public /*@pure@*/ boolean isLinked(MyPerson person) {
if (person.getId() == id) {
return true;
}
for (MyPerson tmpPerson:acquaintance
) {
if (tmpPerson.getId() == person.getId()) {
return true;
}
}
return false;
}
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId());
@ assignable \nothing;
@ ensures (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId() && \result == value[i]);
@ also
@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() != person.getId());
@ ensures \result == 0;
@*/
public /*@pure@*/ int queryValue(MyPerson person) {
int index = -1;
for (int i = 0;i < size; i++) {
if (acquaintance[i].getId() == person.getId()) {
index = i;
}
}
if (index != -1) {
return value[index];
}
else {
return 0;
}
}
//@ ensures \result == acquaintance.length;
public /*@pure@*/ int getAcquaintanceSum() {
return size;
}
/*@ also
@ public normal_behavior
@ ensures \result == name.compareTo(p2.getName());
@*/
public /*@pure@*/ int compareTo(MyPerson p2) {
return name.compareTo(p2.getName());
}
/*public HashMap getAcquaintance() {
return acquaintance;
}*/
public /*@pure@*/ void addAcquaintance(MyPerson p2,int val) {
acquaintance[size] = p2;
value[size] = val;
size++;
}
}
最后测试了isLinked
,queryValue
和getAcquaintanceSum
三个方法,没有报错,大概是跑通了吧。
我自己的感受是,如果SMT Solver只能死板地根据jml规定的数据结构对代码进行检验的话,在实际工程中的实用性或许并不高。毕竟往往JML里的”数组“只是指代一个集合,但是编程人员的实现这一集合的形式却是多种多样的。不过用OpenJML去检验JML编写的正确性还是可以的。
三、JMLUnitNG使用分析
这一部分我通过把jmlunitng-1_4.jar
与第三次作业的java文件放在OpenJML的文件夹下,再运行以下三条命令来实现。
java -jar jmlunitng-1_4.jar *.java
javac -cp jmlunitng-1_4.jar *.java
java -cp jmlunitng-1_4.jar MyGroup_JML_Test
最后得到的结果如下
根据输出结果,我大概推测,jmlunitng的工作原理是针对每个类生成总的测试程序,再生成每个方法的测试程序,进行测试。然而也可以很明显的看出来,对于方法参数为int
类型的,测试数据只有0, 2147483647, -2147483648
三种,对于参数为Object
的,则传入一个随机的Object与null进行测试,如果传入参数类型为其他类型,则只会测试null的数据。笔者认为JMLUnitNG适合在传入参数为int时进行边界测试,但也仅此而已了。它远远不能达到自动化测试的要求,在数据覆盖程度上,甚至无法与手写的JUnit代码相比。
四、三次作业分析
说来惭愧,三次作业我基本都是在MyPerson,MyGroup,MyNetwork的简单架构下进行的,除了第三次作业为了使用tarjan算法和迪杰斯特拉算法引入了Path类以外,并没对架构进行思考,这是我这三次作业做得不好的地方。
第一次作业
第一次作业比较简单,几乎照着规格写代码就行,唯一需要考虑的是isCircle方法,开始我使用了dfs,后来改为bfs进行搜索。
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
MainClass | main | 15 | 1 | 1 |
MyNetwork | contains | 3 | 1 | 1 |
MyNetwork | getPerson | 8 | 2 | 1 |
MyNetwork | addPerson | 6 | 2 | 1 |
MyNetwork | addRelation | 13 | 4 | 3 |
MyNetwork | queryValue | 9 | 3 | 2 |
MyNetwork | queryConflict | 6 | 2 | 2 |
MyNetwork | queryAcquaintanceSum | 6 | 2 | 1 |
MyNetwork | compareAge | 6 | 2 | 2 |
MyNetwork | compareName | 6 | 2 | 2 |
MyNetwork | queryPeopleSum | 3 | 1 | 0 |
MyNetwork | queryNameRank | 14 | 4 | 1 |
MyNetwork | isCircle | 32 | 7 | 2 |
MyPerson | MyPerson | 6 | 1 | 4 |
MyPerson | getId | 3 | 1 | 0 |
MyPerson | getName | 3 | 1 | 0 |
MyPerson | getCharacter | 3 | 1 | 0 |
MyPerson | getAge | 3 | 1 | 0 |
MyPerson | isLinked | 3 | 1 | 1 |
MyPerson | queryValue | 8 | 2 | 1 |
MyPerson | getAcquaintanceSum | 3 | 1 | 0 |
MyPerson | compareTo | 3 | 1 | 1 |
MyPerson | getAcquaintance | 3 | 1 | 0 |
MyPerson | addAcquaintance | 4 | 1 | 2 |
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
MainClass | 0 | 0 | 1 | 1 | 17 | 1 | 0 | 0 | -1 | 0 | 1 |
MyNetwork | 3 | 0 | 12 | 12 | 117 | 32 | 0 | 1 | 0.666667 | 0 | 5 |
MyPerson | 6 | 0 | 11 | 11 | 50 | 12 | 0 | 1 | 0 | 0 | 1 |
Test2 | 3 | 0 | 19 | 19 | 100 | 19 | 0 | 0 | 0.105263 | 0 | 0 |
第一次作业的度量也比较好看,没有出现大问题。
第二次作业
第二次作业笔者刚开始实现时照抄规格,导致queryRelationSum和queryValueSum双重循环,时间复杂度爆炸。好在与同学对拍后发现了这个问题,于是在addPersonToGroup和addRelation的时候动态维护了relationSum和valueSum两个变量,使这两个方法的时间复杂度从O(n)降低到了O(1),大大提高了速度。此外还维护了ageSum与ageSquareSum两个变量,也让queryAgeSum和queryAgeVar两个方法的时间复杂度降低到了O(1)。
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
MainClass | main | 15 | 1 | 1 |
MyGroup | MyGroup | 7 | 1 | 1 |
MyGroup | getId | 3 | 1 | 0 |
MyGroup | addPerson | 22 | 5 | 1 |
MyGroup | hasPerson | 3 | 1 | 1 |
MyGroup | addRelationSum | 3 | 1 | 0 |
MyGroup | addValueSum | 3 | 1 | 1 |
MyGroup | getRelationSum | 3 | 1 | 0 |
MyGroup | getValueSum | 3 | 1 | 0 |
MyGroup | getConflictSum | 17 | 4 | 0 |
MyGroup | getAgeMean | 7 | 2 | 0 |
MyGroup | getAgeVar | 10 | 2 | 0 |
MyGroup | equals | 7 | 2 | 1 |
MyGroup | getPeople | 3 | 1 | 0 |
MyNetwork | contains | 3 | 1 | 1 |
MyNetwork | getPerson | 8 | 2 | 1 |
MyNetwork | addPerson | 7 | 2 | 1 |
MyNetwork | addRelation | 24 | 6 | 3 |
MyNetwork | queryValue | 9 | 3 | 2 |
MyNetwork | queryConflict | 6 | 2 | 2 |
MyNetwork | queryAcquaintanceSum | 6 | 2 | 1 |
MyNetwork | compareAge | 6 | 2 | 2 |
MyNetwork | compareName | 6 | 2 | 2 |
MyNetwork | queryPeopleSum | 3 | 1 | 0 |
MyNetwork | queryNameRank | 14 | 4 | 1 |
MyNetwork | isCircle | 6 | 2 | 2 |
MyNetwork | unionFind | 7 | 2 | 1 |
MyNetwork | addGroup | 6 | 2 | 1 |
MyNetwork | addtoGroup | 17 | 5 | 2 |
MyNetwork | getGroup | 3 | 1 | 1 |
MyNetwork | queryGroupSum | 3 | 1 | 0 |
MyNetwork | queryGroupPeopleSum | 6 | 2 | 1 |
MyNetwork | queryGroupRelationSum | 6 | 2 | 1 |
MyNetwork | queryGroupValueSum | 6 | 2 | 1 |
MyNetwork | queryGroupConflictSum | 6 | 2 | 1 |
MyNetwork | queryGroupAgeMean | 6 | 2 | 1 |
MyNetwork | queryGroupAgeVar | 6 | 2 | 1 |
MyNetwork | sameGroup | 11 | 3 | 2 |
MyPerson | MyPerson | 6 | 1 | 4 |
MyPerson | addGroup | 3 | 1 | 1 |
MyPerson | getGroups | 3 | 1 | 0 |
MyPerson | getId | 3 | 1 | 0 |
MyPerson | getName | 3 | 1 | 0 |
MyPerson | getCharacter | 3 | 1 | 0 |
MyPerson | getAge | 3 | 1 | 0 |
MyPerson | isLinked | 3 | 1 | 1 |
MyPerson | queryValue | 8 | 2 | 1 |
MyPerson | getAcquaintanceSum | 3 | 1 | 0 |
MyPerson | compareTo | 3 | 1 | 1 |
MyPerson | getAcquaintance | 3 | 1 | 0 |
MyPerson | addAcquaintance | 4 | 1 | 2 |
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
MainClass | 0 | 0 | 1 | 1 | 17 | 1 | 0 | 0 | -1 | 0 | 1 |
MyGroup | 6 | 0 | 13 | 13 | 99 | 23 | 0 | 1 | 0 | 0 | 1 |
MyNetwork | 3 | 0 | 24 | 24 | 186 | 55 | 0 | 1 | 0.291667 | 0 | 8 |
MyPerson | 7 | 0 | 13 | 13 | 57 | 14 | 0 | 1 | 0 | 0 | 1 |
第二次作业的度量看起来也比较好看。
第三次作业
本次作业动态维护了blockSum变量,在addPerson和addRelation时加以改变,从而在O(1)的时间复杂度实现了queryBlockSum。同时用堆优化的迪杰斯特拉算法实现了queryMinPath方法,学习了一波tarjan算法实现了queryStrongLink方法。
度量分析,本次作业存在一个布尔表达式过于复杂的问题。
Method Name | Implementation Smell | Cause of the Smell |
---|---|---|
queryMinPath | Complex Conditional | The conditional expression !visited.contains(key) && (!distance.containsKey(key) || distance.get(key) > (distance.get(nowVisit)) + tmpValue) is complex. |
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
MainClass | 0 | 0 | 1 | 1 | 17 | 1 | 0 | 0 | -1 | 0 | 1 |
MyGroup | 7 | 0 | 14 | 14 | 110 | 25 | 0 | 1 | 0 | 0 | 1 |
MyNetwork | 17 | 0 | 33 | 33 | 394 | 103 | 0 | 1 | 0.30303 | 0 | 8 |
MyPerson | 7 | 0 | 14 | 14 | 60 | 15 | 0 | 1 | 0 | 0 | 1 |
Path | 3 | 0 | 4 | 4 | 19 | 4 | 0 | 0 | 0 | 0 | 0 |
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
MainClass | main | 15 | 1 | 1 |
MyGroup | MyGroup | 7 | 1 | 1 |
MyGroup | getId | 3 | 1 | 0 |
MyGroup | addPerson | 23 | 5 | 1 |
MyGroup | hasPerson | 3 | 1 | 1 |
MyGroup | addRelationSum | 3 | 1 | 0 |
MyGroup | addValueSum | 3 | 1 | 1 |
MyGroup | getRelationSum | 3 | 1 | 0 |
MyGroup | getValueSum | 3 | 1 | 0 |
MyGroup | getConflictSum | 3 | 1 | 0 |
MyGroup | getAgeMean | 7 | 2 | 0 |
MyGroup | getAgeVar | 10 | 2 | 0 |
MyGroup | delPerson | 23 | 5 | 1 |
MyGroup | equals | 7 | 2 | 1 |
MyGroup | getPeople | 3 | 1 | 0 |
MyNetwork | compare | 9 | 3 | 2 |
MyNetwork | contains | 3 | 1 | 1 |
MyNetwork | getPerson | 8 | 2 | 1 |
MyNetwork | addPerson | 14 | 2 | 1 |
MyNetwork | addRelation | 35 | 8 | 3 |
MyNetwork | queryValue | 9 | 3 | 2 |
MyNetwork | queryConflict | 6 | 2 | 2 |
MyNetwork | queryAcquaintanceSum | 6 | 2 | 1 |
MyNetwork | compareAge | 6 | 2 | 2 |
MyNetwork | compareName | 6 | 2 | 2 |
MyNetwork | queryPeopleSum | 3 | 1 | 0 |
MyNetwork | queryNameRank | 14 | 4 | 1 |
MyNetwork | isCircle | 6 | 2 | 2 |
MyNetwork | unionFind | 7 | 2 | 1 |
MyNetwork | addGroup | 6 | 2 | 1 |
MyNetwork | addtoGroup | 17 | 5 | 2 |
MyNetwork | getGroup | 3 | 1 | 1 |
MyNetwork | queryGroupSum | 3 | 1 | 0 |
MyNetwork | queryGroupPeopleSum | 6 | 2 | 1 |
MyNetwork | queryGroupRelationSum | 6 | 2 | 1 |
MyNetwork | queryGroupValueSum | 6 | 2 | 1 |
MyNetwork | queryGroupConflictSum | 6 | 2 | 1 |
MyNetwork | queryGroupAgeMean | 6 | 2 | 1 |
MyNetwork | queryGroupAgeVar | 6 | 2 | 1 |
MyNetwork | sameGroup | 11 | 3 | 2 |
MyNetwork | queryAgeSum | 12 | 3 | 2 |
MyNetwork | delFromGroup | 12 | 4 | 2 |
MyNetwork | queryMinPath | 32 | 7 | 2 |
MyNetwork | tarjan | 49 | 12 | 2 |
MyNetwork | queryStrongLinked | 41 | 11 | 2 |
MyNetwork | queryBlockSum | 3 | 1 | 0 |
MyNetwork | borrowFrom | 10 | 3 | 3 |
MyNetwork | queryMoney | 6 | 2 | 1 |
MyPerson | MyPerson | 6 | 1 | 4 |
MyPerson | addGroup | 3 | 1 | 1 |
MyPerson | delGroup | 3 | 1 | 1 |
MyPerson | getGroups | 3 | 1 | 0 |
MyPerson | getId | 3 | 1 | 0 |
MyPerson | getName | 3 | 1 | 0 |
MyPerson | getCharacter | 3 | 1 | 0 |
MyPerson | getAge | 3 | 1 | 0 |
MyPerson | isLinked | 3 | 1 | 1 |
MyPerson | queryValue | 8 | 2 | 1 |
MyPerson | getAcquaintanceSum | 3 | 1 | 0 |
MyPerson | compareTo | 3 | 1 | 1 |
MyPerson | getAcquaintance | 3 | 1 | 0 |
MyPerson | addAcquaintance | 4 | 1 | 2 |
Path | Path | 5 | 1 | 3 |
Path | getDistance | 3 | 1 | 0 |
Path | getEnd | 3 | 1 | 0 |
Path | getStart | 3 | 1 | 0 |
其他度量还是比较好看。
五、bug修复情况
自己的bug
三次作业中测都是一次过,互测没问题,可惜第三次作业强测不知道为啥堆优化的迪杰斯特拉算法强测ctle了三个集中测试queryMinPath方法的点,三个数据在自己电脑上跑都是一秒左右就可以跑完,debug时提交同样的commit也实现了,只能自认倒霉了: (
互测屋中的bug
互测时第一次没有发现bug,第二次通过构造特殊数据打中了两次循环的同学。第三次完全依赖我自己搭的评测机打到了同学的queryStrongLink,queryMinPath,queryAgeVar的错误。
自测方法简析
本次作业我主要尝试了JUnit与自建评测机对拍的方式进行自测。感觉JML最棒的在于提供的约束都是以布尔表达式的形式体现的,这样就可以利用JUnit里的assertTrue方法进行测试。这次作业依然迭代开发了评测机,并发布在了GitHub上,评测机无论在自己找bug还是互测都发挥了很大的作用。个人感觉JUnit依赖人工的程度比较大,方便性相较自建评测机还是差一些。
六、心得体会
实验课虽然也有撰写JML,但是总的训练量还是比较小。阅读JML方面,个人感觉可以通过这样的顺序阅读,撰写代码时会快一些:
- 看
\signals
,把异常情况先进行处理 - 看
\requires
,看前置条件 - 看
\assign
,考虑副作用情况 - 看
\ensure
里与\old
有关的部分,看它的变化如何 - 随后看其他成分。
三次作业以这样的感觉阅读JML,至少没有出现错误理解JML产生的问题。
关于JML,我觉得确实是一个同过严谨的语言来规范方法、规范数据的方式,JML在团队工作交流时价值比较高。但可惜的是,OpenJML,JMLUnitNG在自动化程度仍然稍逊一筹,如何构建一套真正自动化的JML测试工具链,依然是一个难度较大的课题。