BUAA_OO 第三单元总结——JML语言
前言
快乐摸鱼的一个月结束了,尽管规格要求读起来还是有点头疼,但相比于前两个单元确实轻松了不少。当然,做作业的时候是不能感到轻松的,不然一定会被bug爆锤...于是虽然写代码没有花费很久时间,但为了保证正确还是花了很大精力去测试,甚至前两个单元一直黑盒测试的我这次居然开始通读代码验证逻辑[摊手],明明OS还没学懂啊......
不得不说人的心理真的很难捉摸,你走进一个满是镜子的屋子,依旧看不真切,只会一次次地在一切结束的时候开始遗憾于自己的种种表现,哀怨于所剩不多的时间。有时候以为自己追求完美,但其实可能只是虚荣心在作祟,毕竟在困难的单元被锤时从来没有感到沮丧,但如果在相对容易的单元翻车估计是会崩心态的,看上去有点儿“欺软怕硬”?只纠结于简单的任务,却对困难任务表现出一种“伪平淡”的态度,细看就有点安于现状,不求上进那味儿了。[逃]
规格实现策略
本单元的简单之处就在于逻辑已经通过另一种语言体系的方式作出了严格的规定,而无需我们自己动脑使之严谨,但也正因如此,保证逻辑严谨的关键就转化成了如何严谨的实现这些规格要求。
一般一个方法的规格包括正常执行和异常两部分,而我习惯于先将异常部分实现,感觉这样在方法的前半部分就能展现出整体的逻辑分布,而不是在一个if下的巨大处理块之后冒出来一个else,至少我看上去感到压抑而混乱(当然,细分出小方法的话当我没说)。
第九次作业时我还只是简单地把requires语句的内容直接放进if语句里,这种做法写起来确实酣畅淋漓,但似乎并不严谨,以至于第九次作业写完还不知道每个方法是干什么用的,过于无脑了。后来的两次作业我都对JML的逻辑进行了自己的变换,在很多&&并列的情况下将判断过程拆分成多层,比如第十一次作业的addMessage方法(虽然比别人多了好多行,但至少读起来通顺多了,而且在梳理逻辑时很容易查到有没有漏掉分支):
public void addMessage(Message message) throws EqualMessageIdException,
EmojiIdNotFoundException, EqualPersonIdException {
if (containsMessage(message.getId())) { //message的id已存在,撞了
throw new MyEqualMessageIdException(message.getId());
} else { //message的id不存在
if (message instanceof EmojiMessage) { //message是表情包
Integer emojiId = ((EmojiMessage) message).getEmojiId();
if (!containsEmojiId(emojiId)) { //表情包不存在,发不了
throw new MyEmojiIdNotFoundException(emojiId);
} else { //表情包存在,看看以啥类型发?
if (message.getType() == 0) { //type:0
if (message.getPerson1().equals(message.getPerson2())) {
throw new MyEqualPersonIdException(message.getPerson1().getId());
} else {
messages.put(message.getId(), message);
}
} else { //type:1
messages.put(message.getId(), message);
}
}
} else { //message不是表情包,那就容易了
if (message.getType() == 0) { //type:0
if (message.getPerson1().equals(message.getPerson2())) {
throw new MyEqualPersonIdException(message.getPerson1().getId());
} else {
messages.put(message.getId(), message);
}
} else { //type:1
messages.put(message.getId(), message);
}
}
}
}
JML规格中,关键成员只是以model的形式给出,并没有限制具体的实现方式,因此需要根据这个成员的具体内容安排合适的变量类型、容器种类,有时候为了不T还需要引入新的变量进行维护。这就需要在“动工”前充分了解整个结构的需要,最好是先阅读规格,了解每个方法的作用、调用情况之后再进行实现,在没有“刀架在脖子上”的时候,“磨刀不误砍柴工”总归是不错的。比如第二次作业需要维护ValueSum变量,它需要在加人、删人、加关系的时候进行维护,如果不对结构有完整的了解,加关系时的维护是很容易忽略的(我是怎么知道的呢?[坏笑])。
基于JML规格的测试
openjml
优秀的课程组代码校验器(bushi)
简单来说,这个工具就是用来测试JML代码的正确性的。文件组成这样的:
test1文件夹里是我的所有类,jmluniting是github上下载的包...
我们来测试一下
嗯,课程组的JML写的很好,没有bug(可是有什么用呢?) 。
再试试写一个错误的JML,证明上面的命令是真的执行了没有报错:
junitng
这个工具主要是用来测试边界数据的,比如根据规格检测到要向函数中传入一个整数变量,就会向函数中传入2147483647,0等边界数据。
依次输入如下指令:
java -jar jmlunitng.jar -cp test1 test1/myNetwork.java
javac -cp jmlunitng.jar test1/*.java
java -jar jmlunitng.jar test1.MyNetwork_JML_Test > output.txt
于是欣喜地看到没有任何报错(鬼知道我为了这没有报错改了几个小时!!!!!)
第一行用于创建测试文件。
第二行用于编译所有java文件(注意需要把所有类的package去掉,相应的import也要去掉(这玩意卡了我一个钟头!!!可能因为生成的测试类没有按照包import我的其他类,我这里按照原有结构就是编译不了,于是干脆全部不要包,爬!)。
第三行运行测试文件,输出结果到output.txt文件。
谢天谢地它终于结束了!!!要吐出来了!!!
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyNetwork()
Failed: <<MyNetwork@6842775d>>.addPerson(null)
Failed: <<MyNetwork@5ecddf8f>>.addRelation(-2147483648, -2147483648, -2147483648)
Failed: <<MyNetwork@27abe2cd>>.addRelation(0, -2147483648, -2147483648)
Failed: <<MyNetwork@5f5a92bb>>.addRelation(2147483647, -2147483648, -2147483648)
Failed: <<MyNetwork@6fdb1f78>>.addRelation(-2147483648, 0, -2147483648)
Failed: <<MyNetwork@51016012>>.addRelation(0, 0, -2147483648)
Failed: <<MyNetwork@29444d75>>.addRelation(2147483647, 0, -2147483648)
Failed: <<MyNetwork@2280cdac>>.addRelation(-2147483648, 2147483647, -2147483648)
Failed: <<MyNetwork@1517365b>>.addRelation(0, 2147483647, -2147483648)
Failed: <<MyNetwork@4fccd51b>>.addRelation(2147483647, 2147483647, -2147483648)
Failed: <<MyNetwork@44e81672>>.addRelation(-2147483648, -2147483648, 0)
Failed: <<MyNetwork@60215eee>>.addRelation(0, -2147483648, 0)
Failed: <<MyNetwork@4ca8195f>>.addRelation(2147483647, -2147483648, 0)
Failed: <<MyNetwork@61baa894>>.addRelation(-2147483648, 0, 0)
Failed: <<MyNetwork@b065c63>>.addRelation(0, 0, 0)
Failed: <<MyNetwork@768debd>>.addRelation(2147483647, 0, 0)
Failed: <<MyNetwork@7d4793a8>>.addRelation(-2147483648, 2147483647, 0)
Failed: <<MyNetwork@449b2d27>>.addRelation(0, 2147483647, 0)
Failed: <<MyNetwork@5479e3f>>.addRelation(2147483647, 2147483647, 0)
Failed: <<MyNetwork@27082746>>.addRelation(-2147483648, -2147483648, 2147483647)
Failed: <<MyNetwork@66133adc>>.addRelation(0, -2147483648, 2147483647)
Failed: <<MyNetwork@7bfcd12c>>.addRelation(2147483647, -2147483648, 2147483647)
Failed: <<MyNetwork@42f30e0a>>.addRelation(-2147483648, 0, 2147483647)
Failed: <<MyNetwork@24273305>>.addRelation(0, 0, 2147483647)
Failed: <<MyNetwork@5b1d2887>>.addRelation(2147483647, 0, 2147483647)
Failed: <<MyNetwork@46f5f779>>.addRelation(-2147483648, 2147483647, 2147483647)
Failed: <<MyNetwork@1c2c22f3>>.addRelation(0, 2147483647, 2147483647)
Failed: <<MyNetwork@18e8568>>.addRelation(2147483647, 2147483647, 2147483647)
Failed: <<MyNetwork@33e5ccce>>.compareName(-2147483648, -2147483648)
Failed: <<MyNetwork@5a42bbf4>>.compareName(0, -2147483648)
Failed: <<MyNetwork@270421f5>>.compareName(2147483647, -2147483648)
Failed: <<MyNetwork@52d455b8>>.compareName(-2147483648, 0)
Failed: <<MyNetwork@4f4a7090>>.compareName(0, 0)
Failed: <<MyNetwork@18ef96>>.compareName(2147483647, 0)
Failed: <<MyNetwork@6956de9>>.compareName(-2147483648, 2147483647)
Failed: <<MyNetwork@769c9116>>.compareName(0, 2147483647)
Failed: <<MyNetwork@6aceb1a5>>.compareName(2147483647, 2147483647)
Passed: <<MyNetwork@2d6d8735>>.contains(-2147483648)
Passed: <<MyNetwork@ba4d54>>.contains(0)
Passed: <<MyNetwork@12bc6874>>.contains(2147483647)
Passed: <<MyNetwork@de0a01f>>.getPerson(-2147483648)
Passed: <<MyNetwork@4c75cab9>>.getPerson(0)
Passed: <<MyNetwork@1ef7fe8e>>.getPerson(2147483647)
Failed: <<MyNetwork@6f79caec>>.isCircle(-2147483648, -2147483648)
Failed: <<MyNetwork@67117f44>>.isCircle(0, -2147483648)
Failed: <<MyNetwork@5d3411d>>.isCircle(2147483647, -2147483648)
Failed: <<MyNetwork@2471cca7>>.isCircle(-2147483648, 0)
Failed: <<MyNetwork@5fe5c6f>>.isCircle(0, 0)
Failed: <<MyNetwork@6979e8cb>>.isCircle(2147483647, 0)
Failed: <<MyNetwork@763d9750>>.isCircle(-2147483648, 2147483647)
Failed: <<MyNetwork@5c0369c4>>.isCircle(0, 2147483647)
Failed: <<MyNetwork@2be94b0f>>.isCircle(2147483647, 2147483647)
Passed: <<MyNetwork@d70c109>>.queryBlockSum()
Failed: <<MyNetwork@17ed40e0>>.queryNameRank(-2147483648)
Failed: <<MyNetwork@50675690>>.queryNameRank(0)
Failed: <<MyNetwork@31b7dea0>>.queryNameRank(2147483647)
Passed: <<MyNetwork@3ac42916>>.queryPeopleSum()
Failed: <<MyNetwork@47d384ee>>.queryValue(-2147483648, -2147483648)
Failed: <<MyNetwork@2d6a9952>>.queryValue(0, -2147483648)
Failed: <<MyNetwork@22a71081>>.queryValue(2147483647, -2147483648)
Failed: <<MyNetwork@3930015a>>.queryValue(-2147483648, 0)
Failed: <<MyNetwork@629f0666>>.queryValue(0, 0)
Failed: <<MyNetwork@1bc6a36e>>.queryValue(2147483647, 0)
Failed: <<MyNetwork@1ff8b8f>>.queryValue(-2147483648, 2147483647)
Failed: <<MyNetwork@387c703b>>.queryValue(0, 2147483647)
Failed: <<MyNetwork@224aed64>>.queryValue(2147483647, 2147483647)
===============================================
Command line suite
Total tests run: 68, Failures: 59, Skips: 0
===============================================
可以看到,Passed寥寥无几,这不废话吗没人加进来肯定异常啊!!!除非我修改测试文件的main函数,那我为什么不直接写JUnit呢???于是后来又测了一份Person(不然都对不起我没吃的那顿晚饭),嗯Passed变多了,至少看着舒心了。。。尽管如此,我还是要说一句,这玩意有啥用??
JUnit测试
这步指导书给的资料已经很详细了,给我一种计组写tb的感觉,它大致就长这个样子:
@Test
public void testAddPerson() throws Exception {
MyNetwork myNetwork = new MyNetwork();
MyPerson myPerson = new MyPerson(1,"abc",100);
myNetwork.addPerson(myPerson);
Assert.assertEquals(myNetwork.contains(1),true);
Assert.assertEquals(myNetwork.contains(100),false);
Assert.assertEquals(myNetwork.getPerson(1),myPerson);
Assert.assertEquals(myNetwork.getPerson(100),null);
}
这个方法就比junitng可爱多了,也确实有些用处,但我觉得我们作业的代码复杂度似乎还是用不上这个强大的工具,于是直接倒戈自动测评机......
容器的选择和使用
做作业前就对本单元的时间复杂度要求有所耳闻,于是除了Dijkstra和个人收到的messages外似乎全部哈希了。因为涉及到查找,所有带id的元素的集合都使用的HashMap,但有一点需要注意,HashMap的containsKey方法是O(1)复杂度,但containsValue方法是O(n)复杂度,稍有不慎直接O(n²)哦。
个人的messages需要按顺序存入和读出,但考虑到外面似乎也没有嵌套其他循环,就直接使用了熟悉的ArrayList。
查找最短距离使用了堆优化的Dijkstra算法,因此使用了java自带的PriorityQueue容器,其可以通过调用方法直接获得堆中的最小元素。
性能问题
性能问题的地方也是容易出bug的地方,那么顺便一起分析了吧。
第九次作业影响性能的地方主要是isCircle方法,查找两人是否再同一连通分量中。暴力递归一定是不可以的,目测bfs或dfs是可行的,而我使用了并查集的方法,维护了一个名为ancestor的HashMap,Key为本人id,Value为祖先id,在addPerson时加入以自己为祖先的键值对,addRelation时将一人的祖先变为另一人的祖先,同时进行路径压缩(如果不压缩,链过长的话在qbs时很可能有递归爆栈的风险)。
身边同学还维护了一个blockSum变量,加人时变量自增,加关系时若两人原本位于不同连通分量则blockSum自减,这样qbs的时间复杂度就由O(n)变成了O(1)。但本次作业我没有维护这个变量,因为当时我一直认为能通过较低复杂度获得的东西没有必要再引入一个新的变量维护,这样很可能因为某处忘记维护导致信息不统一,当然后两次作业想了想,这个维护也并不很危险,于是又倒戈了。
第十次作业性能问题主要来自平均值和方差的计算,为提高性能,我维护了valueSum,ageSum,timeSum(年龄的平方和)三个变量,对于年龄的均值和方差,维护和读取都是O(1)复杂度,当然只需要在加人和删人的时候简单维护一下。但这里需要注意方差的计算方法,如果只根据方差公式将其化简为平方的均值减去均值的平方,将不符合JML规格的要求,因为均值的计算结果是整数,在这期间经历了一个精度损失的过程,公式展开后在数学意义上相等的项在这里是不能合并的,于是只能这样写(平均数 * size ≠ sum):
return (timeSum - 2 * ageSum * getAgeMean() +
getAgeMean() * getAgeMean() * people.size()) / people.size();
根据规格,valueSum是将每对关系计算两次,即 i 与 j 是熟人时加一次value,同时 j 与 i 也是熟人,还需要再加一次,因此若用维护的方式,需要在每次加人时valueSun加2 * value。除了在加人删人外,在添加关系时需要遍历所有group维护valueSum。
第十一次作业主要影响性能的是最短路径计算,如果采用普通的Dijkstra算法,时间复杂度为O(n²),因此追随学长学姐的脚步,我采用了堆优化的Dijkstra算法,使用堆状的优先队列,主要节省了寻找最短边的时间,将时间复杂度降为O(mlogn)。
此外在互测时我发现有同学在for循环中判断containsValue(如下),这无异于O(n²),会爆T。
for (int i = 0; i < a.size(); i++) {
if(a.containsValue(b)) {
//TODO
}
}
架构设计
突然发现想提的内容已经在上一点说得差不多了
第九次作业
-
引入了一个计数类,包含一个id成员,一个<id - 次数>的HashMap,整个类作为静态变量存在于每个异常类中(而不是作为一个独立的静态类,不需要存储异常类型),Counter重载了两个addSelf方法,用于一个id和两个id的统计,在抛出新的异常时就在Counter类中作计数。
public class Counter { private Integer count = 0; private HashMap<Integer, Integer> counts = new HashMap<>(); public void addSelf(Integer id) { count++; if (counts.containsKey(id)) { Integer temp = counts.remove(id) + 1; counts.put(id, temp); } else { counts.put(id, 1); } } public void addSelf(Integer id1, Integer id2) { addSelf(id1); addSelf(id2); count--; } public Integer getCount() { return count; } public Integer getIdCount(Integer id) { return counts.get(id); } }
-
采用并查集压缩路径,降低isCircle方法的时间复杂度。
第十次作业
- 维护ageSum和timeSum变量,ap,dfg时维护。
- 维护valueSum变量,ap,dfg,ar时维护。
public void addPerson(Person person) {
people.put(person.getId(), person);
for (Person existPerson : people.values()) {
valueSum += (2 * person.queryValue(existPerson));
}
ageSum += person.getAge();
timeSum += (person.getAge() * person.getAge());
}
public void delPerson(Person person) {
people.remove(person.getId());
for (Person existPerson : people.values()) {
valueSum -= (2 * person.queryValue(existPerson));
}
ageSum -= person.getAge();
timeSum -= (person.getAge() * person.getAge());
}
public void updateValue(Person person1, Person person2) {
valueSum += (2 * person1.queryValue(person2));
}
第十一次作业
-
引入MyMap类存储图结构,采用<id,<熟人id,value>>的邻接表式存储。内部包含静态Comparator成员,重写compare方法。
private static Comparator<Node> distanceComparator = new Comparator<Node>() { @Override public int compare(Node o1, Node o2) { return o1.getDistance() - o2.getDistance(); } };
-
堆优化的Dijkstra算法引入一个Node类,存储结点id和结点相对于起点的路径距离(理论上可以隐藏在类内部实现,但我担心被Checkstyle卡住,就直接造了一个独立存在的类,其实它的地位没有那么高...)
整体的UML类图如下:
自动化测试
本次主要是对拍,测评机结构并不复杂,主要是生成数据,如果仅仅随机生成会运行出很多异常,没有多大的测试意义,于是在小伙伴搭建了一个记录信息的简单结构,在此基础上根据人员或关系的存在情况生成有条件的数据。直接一波多人共享数据生成,被带飞~这里举一个测试emoji相关指令的数据生成代码:
def testEmojiData():
id1 = 0
id2 = 0
gid = 0
mid = 0
eid = 0
i = 0
for j in range(200):
id1 = random.randint(1, 1000)
ap(id1, id2, gid, mid, eid)
if id1 not in personList:
personList.append(id1)
i += 1
for j in range(1000):
tmp = random.randint(1, 5)
if(tmp < 2):
id1 = random.randint(1, 1000)
id2 = random.randint(1, 1000)
ar(id1, id2, gid, mid, eid)
else:
id1 = random.choice(personList)
id2 = random.choice(personList)
ar(id1, id2, gid, mid, eid)
if (id1, id2) not in relationList:
relationList.append((id1, id2))
i += 1
for j in range(20):
gid = random.randint(1, groupMax)
ag(id1, id2, gid, mid, eid)
if gid not in groupList:
groupList.append(gid)
i += 1
for j in range(200):
tmp = random.randint(1, 5)
if(tmp < 2):
id1 = random.randint(1, 1000)
gid = random.randint(1, groupMax)
atg(id1, id2, gid, mid, eid)
else:
id1 = random.choice(personList)
gid = random.choice(groupList)
atg(id1, id2, gid, mid, eid)
i += 1
while(i < 5000):
index = random.randint(1, 11)
if index == 1:
eid = random.randint(1, 1000)
sei(id1, id2, gid, mid, eid)
if eid not in emojiList:
emojiList.append(eid)
elif index == 2:
dce(id1, id2, gid, mid, eid)
for kk in emojiList:
eid = kk
qp(id1, id2, gid, mid, eid)
i += 1
for kk in range(5):
eid = random.randint(1, 1000)
sei(id1, id2, gid, mid, eid)
i += 1
if messageList == []:
break
else:
mid = random.choice(messageList)
id1 = random.choice(personList)
id2 = random.choice(personList)
aem_0(id1, id2, gid, mid, eid)
i += 1
for kk in range(5):
eid = random.randint(1, 1000)
sei(id1, id2, gid, mid, eid)
i += 1
if messageList == []:
break
else:
mid = random.choice(messageList)
id1 = random.choice(personList)
gid = random.randint(1, groupMax)
aem_1(id1, id2, gid, mid, eid)
i += 1
emojiList.clear()
elif index == 3:
if emojiList == []:
continue
else:
eid = random.choice(emojiList)
qp(id1, id2, gid, mid, eid)
elif index == 4:
if emojiList == []:
continue
else:
eid = random.choice(emojiList)
mid = random.randint(1, 1000)
id1 = random.choice(personList)
id2 = random.choice(personList)
aem_0(id1, id2, gid, mid, eid)
if mid not in messageList:
messageList.append(mid)
elif index == 5:
if emojiList == []:
continue
else:
eid = random.choice(emojiList)
mid = random.randint(1, 1000)
id1 = random.choice(personList)
gid = random.choice(groupList)
aem_1(id1, id2, gid, mid, eid)
if mid not in messageList:
messageList.append(mid)
elif index == 6:
tmp = random.randint(1, 5)
if tmp < 2:
mid = random.randint(1, 1000)
else:
if messageList == []:
continue
else:
mid = random.choice(messageList)
sm(id1, id2, gid, mid, eid)
if mid in messageList:
messageList.remove(mid)
elif index == 7:
tmp = random.randint(1, 5)
if tmp < 2:
mid = random.randint(1, 1000)
else:
if messageList == []:
continue
else:
mid = random.choice(messageList)
sim(id1, id2, gid, mid, eid)
if mid in messageList:
messageList.remove(mid)
elif index == 8:
id1 = random.randint(1, 1000)
ap(id1, id2, gid, mid, eid)
if id1 not in personList:
personList.append(id1)
elif index == 9:
tmp = random.randint(1, 5)
if(tmp < 2):
id1 = random.randint(1, 1000)
id2 = random.randint(1, 1000)
ar(id1, id2, gid, mid, eid)
else:
id1 = random.choice(personList)
id2 = random.choice(personList)
ar(id1, id2, gid, mid, eid)
if (id1, id2) not in relationList:
relationList.append((id1, id2))
elif index == 10:
gid = random.randint(1, groupMax)
ag(id1, id2, gid, mid, eid)
if gid not in groupList:
groupList.append(gid)
elif index == 11:
tmp = random.randint(1, 5)
if(tmp < 2):
id1 = random.randint(1, 1000)
gid = random.randint(1, groupMax)
atg(id1, id2, gid, mid, eid)
else:
id1 = random.choice(personList)
gid = random.choice(groupList)
atg(id1, id2, gid, mid, eid)
i += 1
(但其实远不如直接读代码来得划算)
总结
最后的互测机会了,很珍惜,每个人的代码都通读了,几乎可以肯定没刀到的人是没有bug的,时间复杂度目测也是没问题的。侥幸用O(n²)的TLE刀了三位同学,但其实在读代码时我发现了一些更值得出刀的问题,比如并查集不压缩路径会爆栈,所有HashMap直接开很大等问题,但由于时间和指令数限制,这些有隐患的问题无法通过互测暴露出来。
对拍的过程还是惊险刺激的,一起对拍的四个人,第十次作业直接全员爆bug无人生还,第十一次更是出现了四个人四种输出结果的魔幻状况[捂脸]。
诚然,这一单元没有大起大落,也没有卷性能分的机会,可能在体验上与前两单元有较大的差别,但对我来说却是一个平衡各科学习的好时机,在OS难度暴涨的时候OO这边的push力度相对放缓,整体体验上还是不错的。