目录
1 JML语言概述
1.1 JML语言理论基础梳理
1.2 JML工具链
2 JML工具链应用
2.1 OpenJML
2.2 SMT Solver
2.3 JMLUnitNG
3 作业分析
3.1 第一次作业
3.2 第二次作业
3.3 第三次作业
3.4 bug发现策略与结果
4 心得体会
1 JML语言概述
1.1 JML语言理论基础梳理
JML是一种规格描述语言,它沿用了java的部分语法,并扩展了自己特有的表达式和语法,方便了对规格进行描述。JML语言的特点主要体现在以下三个层面:
①表达式:
由于规格语言相较于编程语言,需要获取在不同情况下的某些特定取值,所以需要设立新的表达式来表示它们。比如为了表达执行结果要满足的要求,需要使用原子表达式\result、\old等等;为了表达一个数组的特性,可以使用量化表达式\forall、\exist、\sum等等。JML表达式还包括集合表达式和操作符,它们都是描述特定数据所需要满足的要求或特点的。这些看似简单的表达式,经过反复的嵌套组合,就可以表达许多复杂的逻辑,就像我们在离散数学中学习的公理系统类似。
②方法规格:
如果表达式是在具体数据层面做规范,那么方法规格就是对某一方法的执行做约束,这种约束是贯穿方法执行始终的。具体而言,可以分为前置条件requires(执行方法所必须满足的条件)、后置条件ensures(方法执行完毕所必须满足的条件)和副作用assignable(方法执行中所必须满足的条件),在这三种语句后使用各种表达式组合,可以完整的描述一个方法执行的逻辑。此外,方法规格中还有描述异常状态下抛出异常的规范,如使用requires和signal子句结合等等。
③类规格:
在方法规格的基础上,类规格需要有对数据成员的定义和声明,例如使用public instance model non_null…来定义,注意,类规格的数据规格对于开发者而言,大多只是起到明确数据类型的指示作用,其具体的实现是非常灵活的,甚至如果死板的按照JML声明的数据类型编程,会无法实现某些功能。此外类规格还增加了不变式子句invariant(表示类的方法执行过程中,某一数据成员始终需要满足的要求)和状态变化约束constraint(表示某一数据成员在被改变前后必须满足的要求),它们结合可以更加明确地阐释类的特性。
※注:以上仅是从初学者的角度梳理JML的框架,具体的语法和细节在JML手册上已经说得很清楚,在此不赘述。
1.2 JML工具链
JML的相关工具链有以下几个:
①OpenJML检查JML的正确性
②SMT Solver检查JML与代码实现的一致性
③JMLUnitNG自动生成测试数据并进行检验
不过经过笔者实际测试,发现它们的功能并不完善,也有许多不太理解的地方,不过总体而言,能够直观感受到它们各自的作用,具体细节见下一节。
2 JML工具链应用
2.1 OpenJML
OpenJML可以检查JML代码的正确性。
下载链接:https://www.openjml.org/downloads/(内含SMT Solver测试包)
指令:java -jar .\openjml.jar -check -dir ..\Unit3HW3\src\com
指令格式:java -jar <openjml.jar的相对路径> -check -dir <JML代码所在包的相对路径>
对第三次作业的JML进行检查,结果如下图:
单独检查Person.java:
没有任何错误。
单独检查Group.java:
可以看出,错误都是找不到Person类,看来还是需要检查整个目录。
整体检查:
在Group.java的JML中报了一个比较类型的错误,简单分析一下,此函数中\result为int类型,而右边是一个三目运算符,其结果应该也是int类型,笔者并不了解交叉类型的INT#1和int之间的区别,但感觉这个错误无关紧要,总体而言没有错误(显然)。
2.2 SMT Solver
SMT Solver可以检查代码实现的等价性,在上述OpenJML的下载中已经部署。
由于SMT Solver需要JML和代码放在一起,我将第三次作业中的Person类的代码和JML整合,进行测试。
指令:java -jar .\openjml.jar -exec .\Solvers-windows\z3-4.7.1.exe -esc .\Person.java
指令格式:java -jar <openjml.jar的相对路径> -exec <Solver可执行文件的相对路径> -esc <待测试代码的相对路径>
在测试的过程中遇到了各种问题,包括但不限于:
①An identifier with private visibility may not be used in a ensures clause with public visibility
解决方法:将数据成员声明改为public,或在private后加入/*@spec_public@*/。
②找不到符号(情况1)
解决方法:这是因为在Person中,我还保留了诸如MyPerson、Group这样的字段进行辅助实现功能,为了测试需要,全部删去即可。
③找不到符号(情况2)
解决方法:在SMT Solver的逻辑中,会直接调用JML中的方法进行检测,所以代码要和JML的数据类型完全一致。如果使用诸如ArrayList、HashMap这样的数据结构就会出现在调用length方法时报错的情况,将这些按规格改为静态数组即可。
为了与以上要求进行匹配,我删去了原来Person类中的一些函数,修改了部分语句的判断和部分数据类型,最终通过了SMT Solver的检测。当然,对于那个一直出现的、令人匪夷所思的“解析时已到达文件结尾”错误,我选择忽略。
最终结果如下:
Person类改动后代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import java.math.BigInteger; 2 3 public class Person { 4 private /*@spec_public@*/ int id; 5 private /*@spec_public@*/ String name; 6 private /*@spec_public@*/ BigInteger character; 7 private /*@spec_public@*/ int age; 8 private /*@spec_public@*/ Person[] acquaintance; 9 private /*@spec_public@*/ int[] value; 10 11 //@ ensures \result == id; 12 public /*@pure@*/ int getId() { 13 return id; 14 } 15 16 //@ ensures \result.equals(name); 17 public /*@pure@*/ String getName() { 18 return name; 19 } 20 21 //@ ensures \result.equals(character); 22 public /*@pure@*/ BigInteger getCharacter() { 23 return character; 24 } 25 26 //@ ensures \result == age; 27 public /*@pure@*/ int getAge() { 28 return age; 29 } 30 31 /*@ also 32 @ public normal_behavior 33 @ requires obj != null && obj instanceof Person; 34 @ assignable \nothing; 35 @ ensures \result == (((Person) obj).getId() == id); 36 @ also 37 @ public normal_behavior 38 @ requires obj == null || !(obj instanceof Person); 39 @ assignable \nothing; 40 @ ensures \result == false; 41 @*/ 42 public /*@pure@*/ boolean equals(Object obj) { 43 if (this == obj) { 44 return true; 45 } 46 if (obj == null || getClass() != obj.getClass()) { 47 return false; 48 } 49 Person person = (Person) obj; 50 return id == person.getId(); 51 } 52 53 /*@ public normal_behavior 54 @ assignable \nothing; 55 @ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length; 56 @ acquaintance[i].getId() == person.getId()) || person.getId() == id; 57 @*/ 58 public /*@pure@*/ boolean isLinked(Person person) { 59 if (person.getId() == id) { 60 return true; 61 } 62 for (int i = 0; i < acquaintance.length; i++) { 63 if (acquaintance[i].equals(person)) { 64 return true; 65 } 66 } 67 return false; 68 } 69 70 //@ ensures \result == acquaintance.length; 71 public /*@pure@*/ int getAcquaintanceSum() { 72 return acquaintance.length; 73 } 74 75 /*@ also 76 @ public normal_behavior 77 @ ensures \result == name.compareTo(p2.getName()); 78 @*/ 79 public /*@pure@*/ int compareTo(Person p2) { 80 return name.compareTo(p2.getName()); 81 }
2.3 JMLUnitNG
JMLUnit可以根据代码自动生成测试样例并执行,但是经实验后发现它的数据点并不受到JML中requires的限制,并且都是数据类型中极端的数据点和Null,个人认为在实际功能评价方面,JMLUnitNG难以具有说服力。以下是运行的具体流程:
首先将待测试的Group.java和MyGroup.java放至一个文件夹下,由于其中引用了Person类,那就将Person.java和MyPerson.java也放进来,并下载jmlunitng的jar包放至同一个文件夹下。注意,为了运行顺利,需要将MyGroup和MyPerson中对于ArrayList和HashMap等数据类型定义处补充完整(people=new ArrayList<Person>())。
然后在命令行依次执行以下三条指令:
①java -jar jmlunitng-1_4.jar test\MyGroup.java
②javac -cp jmlunitng-1_4.jar test\*.java
③java -cp jmlunitng-1_4.jar test.MyGroup_JML_Test
运行结果为:
可以看出,所有的测试数据构造的核心思想都是null、整数最大值和最小值以及0,而所有Failed的数据点都是由于执行了方法规格requires中没有规定的情况,所以没有什么参考价值。如果想真正检测方法执行是否正确,那么进行黑盒测试并对拍是最好的选择,当然编写Junit进行白盒测试也十分有效。而对目前已有的JML工具链,显然还有进一步完善的空间。
3 作业分析
3.1 第一次作业
架构设计:
第一次作业较为简单,需要我们模拟一个虚拟社交系统,实际上就是一个带边权值的无向图。在MyPerson类中,由于每个人的好友不会重复,我使用了HashSet存储相邻结点,使用HashMap存储与每一个相邻结点连接的边的权值,加快查找速度。在MyNetwork类中,我使用HashMap以id为键值存储图中的每一个结点,同样是便于查找。
第一次作业中有以下几个需要注意的方法:
①isLinked:这个方法并不难,通过参数person在HashSet查找即可,不过值得注意的是如果参数就是该对象自身,需要返回true。
②isCircle:这是一个判断两结点是否连通的方法,我使用bfs从id1结点遍历,判断id2能否到达,注意id1和id2相等时放回true。
对于每个方法中可能抛出的异常,需要首先根据JML中的exceptional_behavior描述对参数进行前置判断,相应的异常已经由课程组给出,按要求抛出即可,不再赘述。
UML类图:
3.2 第二次作业
架构设计:
第二次作业添加了群组的概念,并在群组中引入了RelationSum,ValueSum等特有数据供查询。此外,由于最大指令条数为10万条,使得这次作业在性能优化上非常重要,一些方法不能再简单地按照JML的逻辑暴力实现,而是需要一些技巧,否则有极大可能超时。
本次作业中,我在群组内设置了相应的数据成员记录群组当前的状态。每当addtoGroup时,对每个状态成员的值进行更新。这种方法还需要考虑两个人分别加入同一个群组,然后再加好友的情况。也就是说在调用addRelation时,需要遍历其中一个人加入的所有组,如果第二个人也在某一组中,那么就要更新这一组的状态。
第二次作业有以下几个需要注意的方法:
①addtoGroup:在我使用的方法中,每次执行向组内加人的方法,都需要通过遍历组内已经存在的成员,判断关系,然后逐一更新该组的状态参数,包括RelationSum、ValueSum、ConflictSum和AgeSum,供查询方法调用。特别需要注意,当组内成员超过1111个人时,不再向组内添加成员。
②getRelationSum:这个函数在实现上只是简单地返回组内的数据成员,但是在实现时需要注意,由于JML中使用isLinked函数判断,所以在给组加人时,首先要把RelationSum加1。
③getAgeMean和getAgeVar:这两个函数看似简单,实则坑点很多。由于JML中使用了先做和,再做地板除的算法,那实现时就必须严格按照这个顺序计算,否则会出现误差。同时,由于使用了地板除,一些进行方差变换的公式也不再适用,只能老老实实先算差,平方求和再做除。最后,需要注意方法中对size为0时的特判。
UML类图:
3.3 第三次作业
架构设计:
第三次作业引入了一些离散数学和数据结构课程中的知识,如查找最短路径、查询连通分量个数、判断两点是否在同一个点双连通分量中,并大幅减少了最大指令条数至3000条,使得完成的重心转移到了一些图论的算法实现上。
第三次作业有以下几个需要注意的方法:
①queryMinPath:求两点间的最短路径,我使用了堆优化的迪杰斯特拉算法。编写了一个新的类PersonNode存储某一结点和从起点到此结点的当前最短路径,并覆写了这个类的compareTo方法,以之构建优先队列,每次出队的元素标记为已经获得了其最短路径。如果优先队列为空时,还没有到达目的地,返回-1标志不连通。
②queryBlockSum:求图中连通分量的个数。这个方法使用并查集较为方便,不过我选择了缓存的方法,比较直观。在MyNetwork设置字段blockSum,addPerson时加1,addRelation时,判断两点是否之前连通,如果不连通,blockSum减1,否则blockSum不变,原理很好理解。
③queryStrongLinked:判断两个结点之间是否有两条完全不同的通路。它等价于判断两点是否同时处于一个点双连通分量中,使用tarjan算法比较好,不过为了直观,我选择了一种复杂度没有tarjan更优的判断+bfs方法。具体而言,先调用isCircle判断是否连通,如果连通,判断两点是否直接连接:如果是,去除这条直连的边再bfs判断是否仍连通,如果不是,去掉图中的除了这两个点之外的每一个点,判断是否仍连通。
UML类图:
3.4 bug发现策略与结果
在本单元中,我着重使用了JUnit单元测试来检测程序的正确性。
使用单元测试的好处是可以分开检测各个不同的方法,快速定位错误,且所有的数据都是自己构造的,可以有针对性的设计一些特例,验证方法执行是否考虑全面。
我在使用单元测试时,会先构造约包含10~20个点的图,将不连通、连通、强连通等所有情况都尽可能涉及到,进行一次状态的查询。在不同的方法中,也可以在一次查询后,动态改变整个图的状态,再次进行查询,检验不同方法组合时的正确性。
下面附上我在第三次作业中单元测试代码的一部分,供参考:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import com.oocourse.spec3.exceptions.EqualPersonIdException; 2 import com.oocourse.spec3.exceptions.EqualRelationException; 3 import com.oocourse.spec3.exceptions.GroupIdNotFoundException; 4 import com.oocourse.spec3.exceptions.PersonIdNotFoundException; 5 import org.junit.BeforeClass; 6 import org.junit.Test; 7 8 import java.math.BigInteger; 9 10 import static org.junit.Assert.*; 11 12 13 /** 14 * MyNetwork Tester. 15 * 16 * @author <DawnCat> 17 * @version 1.0 18 * @since 05/14/2020 19 */ 20 public class MyNetworkTest { 21 private static MyNetwork myNetwork = new MyNetwork(); 22 private static MyPerson person1 = new MyPerson(1, "a", BigInteger.ONE, 15); 23 private static MyPerson person2 = new MyPerson(2, "b", new BigInteger("2"), 16); 24 private static MyPerson person3 = new MyPerson(3, "c", new BigInteger("3"), 17); 25 private static MyPerson person4 = new MyPerson(4, "d", new BigInteger("4"), 18); 26 private static MyPerson person5 = new MyPerson(5, "e", new BigInteger("5"), 19); 27 private static MyPerson person6 = new MyPerson(6, "f", new BigInteger("6"), 20); 28 private static MyPerson person7 = new MyPerson(7, "g", new BigInteger("7"), 21); 29 private static MyPerson person8 = new MyPerson(8, "h", new BigInteger("8"), 22); 30 private static MyPerson person9 = new MyPerson(9, "i", new BigInteger("9"), 23); 31 private static MyPerson person10 = new MyPerson(10, "j", new BigInteger("10"), 24); 32 private static MyPerson person11 = new MyPerson(11, "k", new BigInteger("11"), 25); 33 private static MyPerson person12 = new MyPerson(12, "l", new BigInteger("12"), 26); 34 private static MyGroup group1 = new MyGroup(1); 35 36 @BeforeClass 37 public static void setUp() throws Exception { 38 myNetwork.addPerson(person1); 39 myNetwork.addPerson(person2); 40 myNetwork.addPerson(person3); 41 myNetwork.addPerson(person4); 42 myNetwork.addPerson(person5); 43 myNetwork.addPerson(person6); 44 myNetwork.addRelation(1, 2, 1); 45 myNetwork.addRelation(2, 3, 1); 46 myNetwork.addRelation(1, 3, 1); 47 myNetwork.addRelation(1, 6, 1); 48 myNetwork.addRelation(3, 4, 1); 49 myNetwork.addRelation(3, 6, 1); 50 myNetwork.addRelation(5, 6, 1); 51 myNetwork.addPerson(person7); 52 myNetwork.addPerson(person8); 53 myNetwork.addPerson(person9); 54 myNetwork.addPerson(person10); 55 myNetwork.addPerson(person11); 56 myNetwork.addPerson(person12); 57 myNetwork.addRelation(7, 8, 7); 58 myNetwork.addRelation(8, 9, 10); 59 myNetwork.addRelation(7, 9, 9); 60 myNetwork.addRelation(7, 12, 14); 61 myNetwork.addRelation(9, 12, 2); 62 myNetwork.addRelation(11, 12, 9); 63 myNetwork.addRelation(9, 10, 11); 64 myNetwork.addRelation(11, 10, 6); 65 myNetwork.addRelation(8, 10, 15); 66 myNetwork.addGroup(group1); 67 } 68 69 /** 70 * Method: isCircle(int id1, int id2) 71 */ 72 @Test 73 public void testIsCircle() throws PersonIdNotFoundException { 74 for (int i = 1; i <= 6; i++) { 75 for (int j = 7; j <= 12; j++) { 76 assertFalse(myNetwork.isCircle(i, j)); 77 } 78 } 79 for (int i = 1; i <= 6; i++) { 80 for (int j = i; j <= 6; j++) { 81 assertTrue(myNetwork.isCircle(i, j)); 82 } 83 } 84 for (int i = 7; i <= 12; i++) { 85 for (int j = i; j <= 12; j++) { 86 assertTrue(myNetwork.isCircle(i, j)); 87 } 88 } 89 } 90 91 92 /** 93 * Method: queryAgeSum(int l, int r) 94 */ 95 @Test 96 public void testQueryAgeSum() { 97 assertEquals(myNetwork.queryAgeSum(18, 20), 3); 98 assertEquals(myNetwork.queryAgeSum(15, 24), 10); 99 assertEquals(myNetwork.queryAgeSum(22, 22), 1); 100 } 101 102 /** 103 * Method: delFromGroup(int id1, int id2) 104 */ 105 @Test 106 public void testDelFromGroup() throws PersonIdNotFoundException, EqualPersonIdException, GroupIdNotFoundException { 107 myNetwork.addtoGroup(1, 1); 108 myNetwork.addtoGroup(3, 1); 109 myNetwork.addtoGroup(5, 1); 110 myNetwork.addtoGroup(7, 1); 111 assertEquals(myNetwork.queryGroupPeopleSum(1), 4); 112 assertEquals(myNetwork.queryGroupRelationSum(1), 6); 113 assertEquals(myNetwork.queryGroupValueSum(1), 2); 114 BigInteger conflict1 = person1.getCharacter().xor(person3.getCharacter()).xor(person5.getCharacter()).xor(person7.getCharacter()); 115 assertEquals(myNetwork.queryGroupConflictSum(1), conflict1); 116 int mean1 = (person1.getAge() + person3.getAge() + person5.getAge() + person7.getAge()) / 4; 117 assertEquals(myNetwork.queryGroupAgeMean(1), mean1); 118 int var1 = ((person1.getAge() - mean1) * (person1.getAge() - mean1) + 119 (person3.getAge() - mean1) * (person3.getAge() - mean1) + 120 (person5.getAge() - mean1) * (person5.getAge() - mean1) + 121 (person7.getAge() - mean1) * (person7.getAge() - mean1)) / 4; 122 assertEquals(myNetwork.queryGroupAgeVar(1), var1); 123 myNetwork.delFromGroup(3, 1); 124 assertEquals(myNetwork.queryGroupPeopleSum(1), 3); 125 assertEquals(myNetwork.queryGroupRelationSum(1), 3); 126 assertEquals(myNetwork.queryGroupValueSum(1), 0); 127 BigInteger conflict2 = person1.getCharacter().xor(person5.getCharacter()).xor(person7.getCharacter()); 128 assertEquals(myNetwork.queryGroupConflictSum(1), conflict2); 129 int mean2 = (person1.getAge() + person5.getAge() + person7.getAge()) / 3; 130 assertEquals(myNetwork.queryGroupAgeMean(1), mean2); 131 int var2 = ((person1.getAge() - mean1) * (person1.getAge() - mean1) + 132 (person5.getAge() - mean1) * (person5.getAge() - mean1) + 133 (person7.getAge() - mean1) * (person7.getAge() - mean1)) / 3; 134 assertEquals(myNetwork.queryGroupAgeVar(1), var2); 135 } 136 137 /** 138 * Method: queryMinPath(int id1, int id2) 139 */ 140 @Test 141 public void testQueryMinPath() throws PersonIdNotFoundException { 142 assertEquals(myNetwork.queryMinPath(1, 4), 2); 143 assertEquals(myNetwork.queryMinPath(5, 4), 3); 144 assertEquals(myNetwork.queryMinPath(1, 6), 1); 145 for (int i = 1; i <= 12; i++) { 146 assertEquals(myNetwork.queryMinPath(i, i), 0); 147 } 148 assertEquals(myNetwork.queryMinPath(7, 8), 7); 149 assertEquals(myNetwork.queryMinPath(7, 9), 9); 150 assertEquals(myNetwork.queryMinPath(7, 10), 20); 151 assertEquals(myNetwork.queryMinPath(7, 11), 20); 152 assertEquals(myNetwork.queryMinPath(7, 12), 11); 153 for (int i = 1; i <= 6; i++) { 154 for (int j = 7; j <= 12; j++) { 155 assertEquals(myNetwork.queryMinPath(i, j), -1); 156 } 157 } 158 } 159 160 /** 161 * Method: queryStrongLinked(int id1, int id2) 162 */ 163 @Test 164 public void testQueryStrongLinked() throws PersonIdNotFoundException { 165 assertTrue(myNetwork.queryStrongLinked(1, 2)); 166 assertTrue(myNetwork.queryStrongLinked(1, 3)); 167 assertTrue(myNetwork.queryStrongLinked(2, 3)); 168 assertFalse(myNetwork.queryStrongLinked(1, 4)); 169 assertFalse(myNetwork.queryStrongLinked(2, 4)); 170 assertFalse(myNetwork.queryStrongLinked(3, 4)); 171 assertTrue(myNetwork.queryStrongLinked(1, 6)); 172 assertTrue(myNetwork.queryStrongLinked(6, 3)); 173 assertTrue(myNetwork.queryStrongLinked(2, 6)); 174 assertFalse(myNetwork.queryStrongLinked(1, 5)); 175 assertFalse(myNetwork.queryStrongLinked(5, 6)); 176 assertFalse(myNetwork.queryStrongLinked(2, 5)); 177 assertFalse(myNetwork.queryStrongLinked(4, 5)); 178 assertFalse(myNetwork.queryStrongLinked(4, 6)); 179 for (int i = 7; i <= 12; i++) { 180 for (int j = i; j <= 12; j++) { 181 assertTrue(myNetwork.queryStrongLinked(i, j)); 182 } 183 } 184 } 185 186 /** 187 * Method: queryBlockSum() 188 */ 189 @Test 190 public void testQueryBlockSum() throws EqualPersonIdException, PersonIdNotFoundException, EqualRelationException { 191 assertEquals(myNetwork.queryBlockSum(), 2); 192 myNetwork.addPerson(new MyPerson(100, "lllll", new BigInteger("121"), 261)); 193 assertEquals(myNetwork.queryBlockSum(), 3); 194 myNetwork.addPerson(new MyPerson(200, "llsll", new BigInteger("121"), 2)); 195 assertEquals(myNetwork.queryBlockSum(), 4); 196 myNetwork.addRelation(100, 200, 1000); 197 assertEquals(myNetwork.queryBlockSum(), 3); 198 } 199 200 /** 201 * Method: queryMoney(int id) 202 */ 203 @Test 204 public void testQueryMoney() throws EqualPersonIdException, PersonIdNotFoundException { 205 myNetwork.borrowFrom(1, 2, 10); 206 myNetwork.borrowFrom(2, 3, 10); 207 assertEquals(myNetwork.queryMoney(2), 0); 208 assertEquals(myNetwork.queryMoney(1), -10); 209 assertEquals(myNetwork.queryMoney(3), 10); 210 myNetwork.borrowFrom(1, 3, 100); 211 assertEquals(myNetwork.queryMoney(1), -110); 212 assertEquals(myNetwork.queryMoney(3), 110); 213 } 214 }
通过JUnit单元测试,我发现了我在第三次作业中犯下的一些低级错误,例如一些状态参数更新时控制块的执行顺序错误。但是,由于单元测试设计的数据量比较少,所以只适用于检测程序的正确性,而不能较好的检测程序的性能。更主要的是,由于测试数据是自己出的,它便无法检测程序是否满足了JML的全部要求,这也是白盒测试的特点。它可能只能检测出一些语法错误或者手误,对于审题错误、算法性能不佳等问题的检验较为弱势。
我在第二次作业中就犯下了审题错误,没有看见题目中对于组内人数1111的最大限制,WA了4个点,比较可惜。此外,在第三次作业中,在我使用JUnit测试程序无误后,又使用了几个指令数远超课程组限制的数据点进行测试,发现我原来使用dfs寻找环路的方法性能奇差,这才改为去除割点的bfs方法。如果仅靠我的JUnit测试,那么第三次作业强测可能就凉凉了。其实双管齐下是最好的办法,在使用单元测试满足正确性的条件下,生成指令数较多的数据,与同学进行对拍是最好的选择。
在互测中,我仅使用自己编写的JUnit进行hack,并未发现有在正确性方面出错的情况,这也印证了我上面的想法。
4 心得体会
JML的学习让我认识到了规格的重要性。每个人都有自己独特的编程风格和套路,而不论自身习惯如何,能够正确完成需求就是好的。JML正是在满足一定灵活性的基础上对编程进行约束的一种强力工具。比起自然语言的注释描述,JML避免了产生歧义,且逻辑清晰,描述较为全面,能够为开发者传递清晰的需求信息。由于在之前接触过类似公理系统的知识框架,JML的入门还算容易,尽管大部分方法的JML都十分冗长(心疼助教),但是读的多了,就能很快反应过来一长段JML想表达什么意思,并且这个意思并不难理解,只是采用形式化的规格语言描述起来有些麻烦罢了。当然,这也锻炼了我们的审题能力,在快速理解的基础上,要能够找到需求的特别之处,比如一些超出常规的需求,要优先考虑其实现,否则将酿成大错。
本单元虽然是以JML为核心,但是代码如何实现却显得比JML本身更难。这需要我们上学期数据结构和离散数学中学习的各种与图论有关的知识和算法的支撑,我们才能实现JML中的需求,这也提示我们身为计算机系学生,手撸各种bfs、dfs、迪杰斯特拉等等算法应该是随随便便的事情,基础还需要巩固。
这一单元中,我总算开展起了自动测试,不论是使用JUnit进行单元测试,还是构造数据后进行对拍,对于我完成任务都有了很大的帮助。希望能继续学习相关工具链的使用,更加熟练地进行测试。