OO Unit Three总结
Written by: 18373541-xiaohan209
JML语言基础及工具链
JML这一单元是比较神奇的一个单元,其中JML的语法是一个重点,因为其与平常的正规代码的语言不太一样,用的都是比较简单的符号,反而有点像离散数学当中的谓词逻辑。本次主要是从JML手册当中进行学习,然后在实际的每次作业当中阅读进行复习,现在总结出比较常见的JML语言基础部分,其实就是对JML手册的一个提炼总结。
方法规格
在方法规格当中,主要是对于一个方法进行描述。这些关键字主要就是一个方法规格当中的框架,框架搭好后,再用描述规格进行填充,表达正确的意图。
名称 | 作用 | 举例 |
---|---|---|
pure | 标记一个方法,表示方法的执行不会有任何副作用 | public /@pure@/ int queryBlockSum(); |
requires | 确保前置条件,检查参数是否满足 | requires contains(id1) && contains(id2); |
assignable | 只有后面的变量可以被修改 | assignable groups; |
ensures | 保证后置条件,方法返回值为正确值 | ensures \result == people.length; |
signal | 抛出一个异常当满足第二个表达式时 | signals (PersonIdNotFoundException e) !contains(id); |
normal_behavior | 表示正常行为 | public normal_behavior |
exceptional_behavior | 表示异常行为 | also public exceptional_behavior |
类型规格
在类型规格当中有不变式invariant和状态变化约束constraint。invariant是指是要求在所有可见状态下都必须满足的特性。虽然JML手册中列举了很多很多可见状态,在此概括可能有部分情况不符合,但是大约可以描述为可见状态就是指修改此变量的方法执行的前后两个时刻。而constraint就特指在方法执行前和执行后的状态变化的约束。也就是要指出执行后比执行前修改了什么状态。
类型规格当中JML手册有这么两个表格,在此搬运过来:
静态成员初始化 | 有状态静态方法 | 有状态构造方法 | 有状态非静态方法 | |
---|---|---|---|---|
static invariant | 建立 | 保持 | 保持 | 保持 |
instance invariant | (无关) | (无关) | 建立 | 保持,除非是finalizer方法 |
静态成员初始化 | 有状态静态方法 | 有状态构造方法 | 有状态非静态方法 | |
---|---|---|---|---|
static constraint | (无关) | 遵从 | 遵从 | 遵从 |
instance constraint | (无关) | (无关) | (无关) | 遵从 |
说实话对于这一部分,只在第三次作业当中简短的出现了一下,所以不甚了解,只是能够根据JML规格得到约束条件,并实现规格。
描述规格
这一部分暂时名字这么起,其实主要是对于一些细节进行描述,有点像离散数学的逻辑语句,因此总结为这类。
"转义"表达式
这一部分都是一个名称,但是带有'\'
,并且不表示任何在JML规格里可以用到的变量。
名称 | 作用 | 举例 |
---|---|---|
\result | 表示函数返回结果 | \result == groups[i] |
\old | 表示方法执行前某个变量的值(此处变量如果为某个对象的引用,只表示对象的地址) | ensures groups.length == \old(groups.length) + 1; |
\nothing | 表示没有任何变量满足要求 | assignable \nothing; |
\forall | 对后面的所有满足第一个条件表达式的,要满足第二个条件表达式 | ensures (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i])); |
\exists | 存在一个元素,使得满足第一个条件表达式且满足第二个条件表达式 | ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].getId() == id); |
\sum | 对满足第一个条件表达式的,对第二个表达式进行求和 | \result==(\sum int i; 1 <= i && i < pathM.length; pathM[i-1].queryValue(pathM[i]))); |
\not_assigned | 用来表示括号中的变量是否在方法执行过程中被赋值 | ensures (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i])); |
此处还有一些没有列出来,因为在这几次作业和实验当中并没有出现,比如手册中提到的\not_modifified
、\typeof(expr)
,这些都是有作用的。当然assignable应该是可以替代\not_assigned
的,但是这些表达式确实功能有限。
另外就是对于\min
,\max
,\product
,这些本来应该是很强大的表达式,可以精简很多表达,但是由于本次作业没有特殊用到的地方,所以也没出现。另外对于\num_of
表达式可以采用\sum
来替代,因此也在作业当中没有出现。
符号集
名称 | 作用 | 举例 |
---|---|---|
<==> | 等价关系 | 暂无 |
== | 最常见的判断是否相等 | 暂无 |
==> | 可以理解为离散数学的蕴含 | 暂无 |
&& | 平常的与逻辑运算符 | requires obj != null && obj instanceof Person; |
|| | 平常的或逻辑运算符 | acquaintance[i].getId() == person.getId()) || person.getId() == id; |
这些符号集只是众多符号的一部分,只不过是最常用的,因此能够经常出现。
工具链
工具链方面JML的工具链并不成熟,有OpenJml以及JMLUnitNG,当然还有这次作业当中唯一用到的JUnit4。JUnit4能用到主要是因为他没有做到真正识别JML规格,而是只是对已有的方法进行一个提取,建造一个可以调用这些方法的类,并自己手搓数据。所以JUnit也是不好进行自动化测试的工具。当然OpenJml和JMLUnitNG虽然更高级但是本次实验根本没有用到,并且在用这两个工具进行测试来写这一篇blog的时候也足足试了3天之久,最后也没有研究透怎么去使用。
最后,说一下,相比于工具链,现在的我还是选择对拍,但我相信工具链会有一天发展得越来越成熟的。因为JML规格的分层限制作用是很规范的,在大项目描述的时候较为管用,甚至以后对JML规格写法可能还有明确要求,甚至还有更高级更抽象的JML的JML,也许会在比较庞大或者设计的代码架构比较宽的时候能用的到。
JML测试
这一部分是对JML工具链的使用。本人用的是MacOS系统。在此首先声明对于三次作业甚至实验,都没有进行完备的工具链测试,尤其是OpenJml和JMLUnitNG。OpenJml官方包中有,但是自己操作的时候完全跑不动,到写此文时才知道OpenJml不能使用全程量词,所以基本上OpenJml的部署并测试基本不太可能。另外本次作业实现当中并没有写package语句,而是进行import,所以在用自动化工具时遇到一些困难自己没有解决,干脆将本次作业实现单独拿出来放到一个新开的工程项目当中。JUnitNG对于Group
的检查也在新的项目中进行操作。
OpenJML
此处完全用了新的一个工程文件,这个里面所实现的功能就是对人数进行记录,实现增减。然后还有每个人的信息,比如年龄等。然后整体实现了一下类似getAgeMean的操作。两个类分别如下:
public class Person {
/*@ public instance model non_null int id;
@ public instance model non_null String name;
@ public instance model non_null int age;
@*/
public int id;
public String name;
public int age;
public Person(int id,String name,int age) {
this.id = id;
this.name = name;
this.age = age;
}
//@ ensures \result == age;
public /*@pure@*/ int getAge() {
return age;
}
}
public class Group {
/*@ public instance model non_null int id;
@ public instance model non_null Person[] people;
@*/
public int id;
public Person[] people;
public int count;
public Group(int id) {
this.count = 0;
this.id = id;
this.people = new Person[100];
}
/*@ public normal_behavior
@ assignable people;
@ ensures people.length == \old(people.length) + 1;
*/
public void addPerson(Person person) {
people[count] = person;
count++;
people[count] = person;
count++;
}
/*@
@ ensures \result == people.length;
@*/
public int getLength() {
return count;
}
/*@ public normal_behavior
@ requires people.length == 0;
@ ensures \result == 0;
@ requires people.length == 0;
@ ensures \result == people[0].getAge() / people.length;
@*/
public /*@pure@*/ int getMean() {
if(people.length == 0) {
return 1;
}
return people[0].getAge() / people.length;
}
}
这里getMean的实现和addPerson的实现是有问题的,也是检查的重点。
首先利用java -jar openjml.jar -check /Users/pgh/Desktop/project/OO\ Project/TestJML/src/*.java
进行JML语言静态检查,然后就会发现一堆报错,比较奇怪的是他的报错会到一些代码中没有的JAVA库中查找,比如specs/java/lang/CharSequence.jml
,进行提示,由于过长不进行截图。另外就是对于JML规格当中出现的字符,这里检查的时候会发现找不到符号比如一些地方用的len
和i
,并且对于转义的原子表达式,比如\result
,\old
等也识别不出来。
然后利用将-check
改为-esc
可以进行程序分析,不注重JML语言。同样在上面的报错基础上又出现了类似的奇怪报错。
再将-esc
改为-rac
会发现报错中竟然又多了specs/java/lang/String.jml
当中的东西,这里不知道是什么原因,最终也不知道怎样去完整检查,于是宣告失败。
JMLUnitNG
首先这个工具是官方包中没有给出的,于是需要自行下载,上网搜索后给出JMLUnitNG下载网址。可以看到最近一次更新是2014年5月,所以这个工具略微有点弃坑的意思。首先利用自行构造的Group类和Person类进行检查。上述以及提出了这两个类的问题。
首先输入java -jar jmlunitng-1_4.jar /Users/pgh/Desktop/project/OO\ Project/TestJML/src/*.java
运行,然后就再次遇到一堆报错。不过这次报错比较合理,都是在Group和Person类当中的报错,从这一点看,已经比OpenJml靠谱了。并且这里还能自动揪出JML语法的错误,比如多加了括号或者少加了分号。报错有几种情况:
- 应该将变量设置成public形式,而不是private形式(不知为何有这个要求,修改了就不会报错了)
- 实际实现的变量和JML当中的变量不能重名!这点很折磨人,对自行构造的两个类很容易修改,但是对于作业当中的
MyGroup
类确实整体修改有些难。
进行修改后,再次运行直接得出结果:
突然多出来这么多文件不知如何下手,最后还是上网搜了一下,并且和同学们交流得到了指导,了解到Person_JML_Test.java
和Group_JML_Test.java
是入口,这些java文件还需要自行编译之后,再运行即可。
在这里本人没有使用命令行来运行,因为在此已经在TestJML/src
下生成了所有需要的文件,有点像将JUnit的测试文件放在了src
文件夹中。此时只需要在IDEA当中的Project Structure
中用添加modules将jmlunitng1-4.jar
导入,然后分别在IDEA中设置入口,运行Person_JML_Test.java
和Group_JML_Test.java
即可。运行结果分别为:
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor Person(-2147483648, null, -2147483648)
Passed: constructor Person(0, null, -2147483648)
Passed: constructor Person(2147483647, null, -2147483648)
Passed: constructor Person(-2147483648, , -2147483648)
Passed: constructor Person(0, , -2147483648)
Passed: constructor Person(2147483647, , -2147483648)
Passed: constructor Person(-2147483648, null, 0)
Passed: constructor Person(0, null, 0)
Passed: constructor Person(2147483647, null, 0)
Passed: constructor Person(-2147483648, , 0)
Passed: constructor Person(0, , 0)
Passed: constructor Person(2147483647, , 0)
Passed: constructor Person(-2147483648, null, 2147483647)
Passed: constructor Person(0, null, 2147483647)
Passed: constructor Person(2147483647, null, 2147483647)
Passed: constructor Person(-2147483648, , 2147483647)
Passed: constructor Person(0, , 2147483647)
Passed: constructor Person(2147483647, , 2147483647)
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
Passed: <>.getAge()
===============================================
Command line suite
Total tests run: 37, Failures: 1, Skips: 0
===============================================
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor Group(-2147483648)
Passed: constructor Group(0)
Passed: constructor Group(2147483647)
Passed: <>.addPerson(null)
Passed: <>.addPerson(null)
Passed: <>.addPerson(null)
Passed: <>.getLength()
Passed: <>.getLength()
Passed: <>.getLength()
Failed: <>.getMean()
Failed: <>.getMean()
Failed: <>.getMean()
===============================================
Command line suite
Total tests run: 13, Failures: 4, Skips: 0
===============================================
可以看出来第一段代码表示Person
类的测试,只有racEnable
未通过,不知道其代表什么,其他的完全满足,通过检测。第二段是Group
代码的测试,有4个失败,其中三个都是getMean
出现了问题,说明能够检测出来getMean
的问题。但是对这么明显的错误addPerson
都检测不出来有些出乎意料。这主要是因为每次Group
地址可以看出来都是使用的不同的Group
,每次getLength
都会是0,同时addPerson
之后并没有之后的检测,并且构造方法也没有对构造之后继续调用函数检测,所以其检查是离散的非常不完整的。也就是这个工具本质上就是利用int
的最大最小值以及指针的null
值进行检测。
这让我联想起一个段子:程序员建立了一个咖啡店,测试员走进来点了null
杯咖啡,2147483847杯null
,还有0杯水,都没问题然后心满意足走了。之后咖啡店营业了,一位顾客走了进来,问了一句:几点了?咖啡店炸了。这就说明我们没有对可能出现的各种组合进行测试,只测试极端的个体数据往往找不不出来一个软件项目的bug在哪。
当然用自己构造的简单JML项目验证过后,还要按照这次博客要求,修改一下MyGroup进行测试。在修改的过程当中除了上面可能出现的错误以外,还有对于0 <= i < people.length
要修改为0 <= i && i < people.length
。代码较长在此不放出,最终测试结果如下:
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(java.lang.Object@1ef7fe8e)
Passed: <>.equals(java.lang.Object@67117f44)
Passed: <>.equals(java.lang.Object@2471cca7)
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getPeopleLength()
Passed: <>.getPeopleLength()
Passed: <>.getPeopleLength()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.updateRelations(-2147483648)
Passed: <>.updateRelations(-2147483648)
Passed: <>.updateRelations(-2147483648)
Passed: <>.updateRelations(0)
Passed: <>.updateRelations(0)
Passed: <>.updateRelations(0)
Passed: <>.updateRelations(2147483647)
Passed: <>.updateRelations(2147483647)
Passed: <>.updateRelations(2147483647)
===============================================
Command line suite
Total tests run: 49, Failures: 7, Skips: 0
===============================================
没有想到这里居然除了racEnable
以外还有六个点没有通过。这是由于addPerson
方法和delPerson
方法没有设置好传递进来的值为null
的情况,在实际的过程中由于调用addPerson
和delPerson
已经在输入接口进行提取后,传递进来并且在MyNetwork
当中进行了判断,确保了传递进来的不为空,所以在我们的三次作业当中并不会出现问题,但是单拿出来就会暴露,所以这就需要我们尽量在每一层都对安全性进行检查,否则可能在一些极端情况下会有不可预知的错误。
另外从上面的数据也可以看出来这个JUnitNG
完全就是对极大的值,极小的值,0,null
进行遍历,看看有没有错误,本质上还是不能对各个方法间相互联系起来并正确实现进行检查,所以我们以后开发当中如果方法众多,确实可以用这个自动化测试工具来测试极端情况,但是真正功能、逻辑上的检查还得我们自己手动检验,可以配合JUnit工具。
JUnit
这一部分是实验当中训练的重点,虽然实验当中并没有完整对每个函数进行了测试,但是在对部分比较针对性的函数,就是JML规格描述比较复杂的地方,自己手动构造数据明显能够比上述两种方法更能发现bug。
比如:
@Test
public void removeFirst() {
MyObject mo1 = new MyObject();
myHeap.add(mo1);
myHeap.removeFirst();
int size = myHeap.getSize();
Assert.assertEquals(0,size);
Assert.assertFalse(myHeap.getElementData().length == 0);
}
这是实验中发现有错误的测试代码,通过Assert断言,以及利用size和0的关系我们可以知道实验中这一部分对于size的调整是有问题的。
再比如:
public void queryStrongLinked() throws PersonIdNotFoundException, EqualRelationException {
network.addRelation(1, 2, 1);
Assert.assertFalse(network.queryStrongLinked(1, 2));
network.addRelation(2, 3, 1);
Assert.assertFalse(network.queryStrongLinked(1, 2));
network.addRelation(1, 3, 1);
Assert.assertTrue(network.queryStrongLinked(1, 2));
network.addRelation(1, 4, 1);
Assert.assertFalse(network.queryStrongLinked(1, 4));
network.addRelation(1, 5, 1);
Assert.assertFalse(network.queryStrongLinked(5, 4));
network.addRelation(4, 5, 1);
Assert.assertTrue(network.queryStrongLinked(5, 4));
Assert.assertFalse(network.queryStrongLinked(2, 5));
Assert.assertFalse(network.queryStrongLinked(3, 4));
}
这里是对queryStrongLinked
方法进行了单独测试,在bug修复部分也提到了并且画出了这个一个两个连通分量有一个公共割点的沙漏图。这里就比较有针对性能进行测试,并且发现bug的可能性很高。因为是我们手动制作的,可以很方便的去调用每一个方法,然后联合起来达到一个效果,但是自动化测试现在并不能很好识别方法之间的联系,只能傻傻地测试极端数据。所以现在JUnit4依旧是我们测试的主要方案。
代码架构设计
首先这几次的代码架构设计基本上是按照JML规格的那几个类的形式实现的。在代码分析当中有几个作业当中的共性问题。第一个还是存在魔数,也就是一些类当中具体实现采用了内部的数字形式,按照最正规的代码风格应该设置为某个全局变量,使其变量名有意义。另外在实现分析当中,认定类当中的方法过于多,变成了一个God
类。但这是在官方给的接口当中需要实现的,因此这一Smell不是作业当中的问题。另外由于过多的方法导致分析MethodMetrics中成员过于复杂。并且对于JUnit测试类也被分析进了Metrics,因此不将其展示出来,主要由于JUnit测试的类相当于输入,分析其风格没有必要。
第九次作业
-
设计策略
首先在设计方面,本次作业做得并不好,主要是第一次根据JML规格来完成项目,并不是很懂JML对于整个类或者方法的制导意义,直接看到描述发现很简单,于是所有的属性和方法直接照搬到了
MyNetwork
和MyPerson
当中。在书写具体实现的时候考虑到每个人所能到达的成员需要在MyNetwork
当中引用,进行搜索,所以需要getAquaintance
函数,与此同时还加了一些JML规格来修饰这个方法(当时以为JML规格和方法是绑定的,有JML规格就有方法,没有就不能写)。最终UML图如下: 在这当中可以看出按照官方接口实现出来的类间耦合度很小,基本上都是返回一个值,另一个类即可用来判断,不会两个类之间互相调用构造方法的环状结构。最终可以梳理出关系Main函数调用了两个函数(在
Runner
类当中),然后MyNetwork
比MyPerson
更顶层,从而实现功能。另外比较有难度的isCircle
函数采用的是dfs
算法,但由于访问标记没有标记好,导致存在bug,在bug修复中改为bfs
方法,更快的同时也更容易实现。 -
复杂度分析
同时代码的smell分析如下:
复杂度主要对
MyNetwork
和Myperson
分析。首先本次方法中行数都控制在了一定范围之内,类的方法虽然有140行,但是是因为方法太多导致的,并且其他参数也保持在了合理的范围之内。代码最多的最复杂的是isCircle
。其中CC达到了6,这是因为最后bfs的时候有多层循环,其中就有一些圈的产生。总的来说复杂度不算高,很合理,其中也有按照JML规格一步一步写的原因,因为JML已经将不同的功能进行分层,并进行限制。
第十次作业
-
设计策略
本次在上一次作业的基础上增加了`Group`类,其中对应了一些属性和方法。这一次看了讨论区的内容,再加上本次的JML规格相对复杂,对其进行了更深入的思考。比如在`MyPerson`当中就添加了一个人所在的各种`Group`,这是JML没有提到的。当然`Group`当中也加了很多属性,用空间节省时间。本次设计策略主要采用的是缓存机制,对`Group`当中的`relationSum`和`valueSum`进行了缓存。同时设计的时候由于将已有关系两人加到群里和对已在群里两人加关系都会进行修改,因此设计了两个函数`updateRelations`和`addPerson`分别可以修改缓存的属性。UML图如下:
在图中可以看出来MyNetwork高于MyPerson和MyGroup,但是在MyPerson和MyGroup当中互相都存了一个HashMap用来表示人加入的群组,和群组当中的人,相当于进行了数据冗余的操作,但同时也增加了循环引用的层次。
-
复杂度分析
对于代码的smell分析如下:
这次方法数量增多,只选出来了10行以上的,10行以下基本上都是直接
return
取出来的一个属性。在这些类当中关键方法上都基本做到了代码不会冗长,另外isCircle
由于继续沿用的上次的写法,所以复杂度依旧不低。但同时对于几种MyGroup
的几种get方法行数在一定范围之内并且复杂度并不高,值得一提的是这里采用的是每次遍历的方法,只有relationSum
和valueSum
进行了缓存,因为对时间的要求并不是极其严格,MyPerson
数量也不会太多,因此能满足就如此实现了。
第十一次作业
-
设计策略
本次作业可以说挑战很多,不仅要求读懂嵌套层数很多的JML规格,还要求算法,因为数据量很大,并且JML规格的实现如果不优化复杂度很高。这甚至涉及到之前作业如果有没采用最省时的算法的,也需要修改。
第一是对于之前作业的缓存机制的再完善。首先将一个组的年龄和,以及年龄的平方和单独存起来了。年龄平方和开成long型变量。以及一个组的
conflicSum
也采用了缓存机制,这样的好处就是利用空间换取遍历的时间。在此需要利用平方的展开公式,以及异或的性质来进行实现。组内增加一个人就需要对relationSum
,valueSum
,ageSum
,ageSquareSum
进行增加,同时要异或conflictSum
,删除一个人对其进行逆操作即可,异或的逆操作还是异或。另外对isCircle
这个函数也进行了缓存设置。其实最理想的是采用并查集进行操作,追溯到祖先节点即可得到这一连通块的所有人员。但是对并查集并不是太了解,于是只是给每个MyPerson
建立了一个HashMap
叫做reach
,代表可以到达的人。这样的速度并没有并查集快,但是要比每次广度优先搜索快。 第二是本次新增加的几个关键函数。
queryMinPath
函数在互测之前都是采用的普通的迪杰特斯拉算法,比较朴素,导致强测超时。在之后的修复当中利用JAVA
自带的PriorityQueue
进行了堆优化。因此在MyNetwork之下又建立了一个NewMinPath
类,用这个类来表示每次加入到优先队列当中的元素,最重要的是要实现比较接口,能够compareTo
另一个变量,实现小顶堆。这样的复杂度是O((e+v)loge),其中e为边数,v为顶点数。queryStrongLinked
函数直接采用了tarjan算法。这个算法在理解的时候花费了较多时间,也因此遇到很多问题,甚至本人直接当做一个话题在研讨课中进行了交流。tarjan算法采用先深搜一遍之后直接用low数组来存每一个点的对应的特殊值,之后通过两个id分别get并比较即可,复杂度为O(e+v),也就是深搜的复杂度。queryBlockSum
函数由于对每个MyPerson存放了可以到达的所有人,所以遍历所有人,如果一个人未被访问,就将其可到达人员添加进来并给计数器加1,直到每个点被添加进来。复杂度为O(v),v为MyPerson的数量。
剩下的就是新增加的一些实现难度不高的借钱等函数,此处省略,以下是UML图。
-
复杂度分析
首先可以看到isCircle方法在这次作业当中代码行数和复杂度都直接下降,是因为采用了冗余可到达的HashMap,reach数据的方式。同时MyNetwork当中的addRelation方法复杂度上升,因为其要实现合并reach的操作。在新增的函数当中,较长的就是queryMinPath和tarjan方法。这两个其实都是深搜的函数,只不过一个是递归调用,一个是表示成了while循环,可见搜索对于整个方法代码长度会有显著提升。不光如此这两个方法的圈复杂度也很高,因为深度搜索的缘故,所以确实不可避免。同时对于MyNetwork类由于实现的方法过于多,总共有28个方法,所以代码过长,其复杂度较高。另外新增的用于存放最短路径的类NewMinPath复杂度很低,仅次于Main方法,可以看到这个类还是较为简单地实现了存储可能的短路径的功能。
整体总结
首先本次作业整体的特点就是三次作业非常完美地迭代开发。对于新增的功能,拓展的功能,完全是按照之前作业的JML规格和实现上新增函数或者新增属性,没有影响到原来的部分。所以检查bug出现错误也很方便。也就是本单元官方给的接口对于OCP原则的实现非常完美,这虽然是上一个单元主要提到的,但上一个单元实现的并不是很好,所以本次也算是给上一次的6个原则做出了示范。同时SRP原则也实现的很好,MyPerson
,MyNetwork
和MyGroup
的功能彼此独立,如果需要沟通只通过一个函数得到返回值然后进行操作。另外本次的里氏替换原则由于只有一层接口实现,所以调用到官方接口的地方都可以换成自己实现的类,不会有冲突。
BUG和修复情况
第九次作业
第一次作业在未提交的时候并没有测试出bug,同时自己也没有写对拍机。于是在互测当中被hack出了错误,主要就是体现在了CTLE
上面。这是由于点和关系过多,导致在isCircle
函数当中会有部分情况结束不了循环,也就是仍有点在遍历的时候没有标记。互测之前我的isCircle
函数采用的是深搜。在我们的社交关系网络图当中最基础的是点,因为在MyNetwork
当中存储了每个人的信息,但是关系信息是在MyPerson
类当中进行存储的。并且社交关系网之间的关系更倾向于图,而不是倾向于树的结构,也就是关联性较高,所以适合广度优先搜索。否则深度优先搜索可能会过度访问祖先而造成一些不可预知的错误。本次的bug也是将dfs
改动为bfs
算法,便可解决所有互测当中出现的bug。
通过这个bug也可以看出来,这个JML规格下的方法可以有多种实现类型,不能单单照着JML规格想到第一个能实现的方法来写即可。反而应该是通读所有的规格代码总结出来本次工程项目的特点,规格只是方法将前置条件下的参数转换成规定的后置条件,只是最起码的下限,具体的实现要结合自己的代码架构来权衡。
第十次作业
根据之前的测试结果,我这次真的完成了评测机!!!这算是OO的这几个单元当中我第一次搭出评测机的一次作业。主要是这单元的数据集都已经给出了,数据间有着并列的关系。只需要选择指令,选择操作数,选择组号或是人的序号,又或者是其他的age
,value
等,即可生成一组完全随机的并且可以进行对拍的数据。
在本次作业当中检查错误主要是本地跑了一些数据进行对拍。其中主要就是queryGroupAgeVar
以及queryGroupAgeMean
经常差1或者差2,这是由于没有仔细看JML规格导致的,已在课下修复。queryGroupRelationSum
和queryGroupValueSum
也产生了问题,此处是看了讨论区以及同学们在群中的发言,了解到了如果每次遍历,不设置缓存的话,可能会超时,因此设置了缓存。但是在缓存的时候没有考虑到,在对拍的时候总是发现比别人的数值少,后来发现是在将两名成员添加进组之后进行addrelation
并没有修改组内的缓存值,而仅仅对人员内部的acquaintance
和value
进行了修改。修复bug的时候在MyPerson
当中增加了对Group的存储,在addrelation
时对每个MyPerson
所在的组更新即可。
最终在互测时没有被hack,强测当中也获得了满分,是本单元最好的一次作业。
第十一次作业
本次作业要实现的函数比较复杂。本次发现bug主要采用的是对拍方法。由于函数太多功能太复杂,所以首先对迭代开发后的代码进行了第十次作业的测试数据集的测试,也就是看能否满足上一次作业的要求。这部分轻松过关。然后将queryBlockSum
,queryMinPath
,queryStrongLinked
从测试数据集中删除,优先测试本次作业的其他方法。这部分也较轻松的过关了,这一部分主要测试的是将MyGroup当中大量的数据进行了缓存后能否正常工作。最终将queryBlockSum
,queryMinPath
,queryStrongLinked
三个方法按顺序每次一点一点加入,进行对拍,比较有无差异。其中在queryStrongLinked
发现的bug次数最多,也就是改动次数最多。
其中数据过长可能在此不显示,但是由于经历了很多的磨砺,在queryStrongLinked
方法当中找出了一些比较经典的错误,在研讨课当中进行了展示,比如:
最终在互测当中没有被hack,但是由于强测的数据比较多,并且没有实现堆优化的迪杰特斯拉算法,因此有三个点超时了,都是超时了大约0.5-0.6秒。这里是由于当时在设计本次架构的时候认为数据量并不是很大,还以为自己要从底层实现小顶堆,并且堆优化可能并不明显,因此铤而走险没有写堆优化,果然超时了。在超时后突然发现原来JAVA自己就有优先队列的数据结构,自然能完成小顶堆的任务,因此直接利用这一个PRIORITYQUEUE
进行了优化,实现步骤反而变得简单了,直接修复了三个数据点,CPU Time
直接从2.6秒降至1.5秒左右,性能提升了40%!可以看到这组数据的完全由ap
,ar
和qmp
组成,明显就是卡堆优化的时间的,因此这种性能提升也确实比较极端。
测试数据总结
另外此处要总结一下本单元是JML单元,按理说应该使用有关JML的工具去自动化测试,但是确实openJML
以及其他的一些JML工具链实在是力不从心,不能去匹配本地的方法,因此放弃了这一条道路。但是这些工具链当中JUnit工具还是比较实用的,在装上之后可以进行手动编写样例进行测试,并且是一个方法一个方法进行测试比较能够准确定位错误。在后两次作业当中也都有用到(第一次主要是没有想到还需要专门的做这种测试,甚至对JUnit还是不甚了解因此没有测试)。在测试的时候也会发现,首先需要"配置环境初始化各种数组",基本上测试的时候up
和down
函数也就是建立和删除的函数当中都会写不少的代码,并且每个函数内部也需要建立许多变量,并且挨个添加。再加上本次JML当中的所有函数都是围绕着MyNetwork
当中的社交关系网而建成的。这就导致测试的时候其实很大一部分时间都是在建立一个图或者是建立一个局部Group
。并且这种JUnit测试主要是用来检验边界条件,或者一些极端数据的。因为JUnit的结果是需要通过断言语句来实现的,因此必须自己验证正确后,才能得到想要的输出,也就是对于某一种情况的验证,JUnit的耗费时间代价远高于对拍机。但是JUnit自己写出来的有针对性的数据是对拍机做不到的,这种数据往往能在一整个软件项目中一直流传,用于迭代开发中验证特殊情况比较有效。
反思心得与体会
这一个单元是一个下限低,上限高的单元。从侧面体现出了,同一个描述的JML规格,具体的实现方案也有很多,并且JML规格是不能与实现完全混为一谈,否则就没必要写JML规格了,直接写出代码即可。这一单元第一大的收获不在于怎样使用JML,而是一定不要跟着JML写!一定要先通读JML之后进行理解,知道这一段规格在描述什么,而不是照着JML规格结合自己类内部的属性方法一个个翻译。同时JML规格的描述受到可以引用的数据、方法的制约,可能比较原始粗暴,这个时候切记不要照搬,否则性能往往是不好的。
其次才是如何书写JML规格。在书写JML规格的时候,一定分清层次,比如方法规格还是类型规格。方法当中哪些是pure标记过的,能够直接引用的,哪些是数据数组是能够直接在JML规格当中写出来的。同时由于可能会嵌套很多层的括号,这就导致逻辑极其复杂。本人认为可以写规格的时候利用缩进空行等来进行书写,最后进行整合。并且写JML规格的时候尽量从整体的角度,也就是前置条件需要满足什么,通过什么变量的修改就能达到后置条件的标准。重点就是前后的两个条件要表明清楚,对于中间的细节可以做到忽略,这样才是完美的JML规格,既不会引导别人做一些无谓的工作,又能清晰地表明这个函数的功能。
在此处也给出一些建议:希望在JML规格当中也能开发出来一个类似checkstyle的东西,能够检查JML规格风格。虽然JML是在注释当中的不能通过编译,但是可以单独分离出来JAVADoc注释,并对其进行一定的语法检查,这样会使得使用JML和书写JML的人更加的规范方便。
另外就是对于其中算法的体会了。感觉这次作业没有之前那种实现上的困难,比如不知道如何提取幂函数,或者函数的套娃,再或者多线程当中的冲突。这次的所有方法都已经标注出来了,但是最终时间的测试是一个重点,因此上限难度很高,中间还需要补充图的知识,学习更深的算法。并且算法上的离散化,记忆化,减少嵌套层次是一个关键的步骤。
以后看到JML规格写代码或者被要求写JML规格就不用慌了,只需要分清两者的角度,前者先了解所有函数的功能,知道正确的对应返回值之后再根据可能的优化进行合理地实现,后者重点需要保证满足各种情况,不能有缺漏,另外一定要再三确认前置条件和后置条件,以及变量修改范围等,只需要用最基础的语言来描述实现即可。
现在JML还不是很成熟,但是他分层思想是非常好的,期待之后越来越成熟,使用者越来越规范,自动测试工具越来越完善的一天。