301
社区成员
发帖
与我相关
我的任务
分享第三单元课程组引入了一种新的解释性语言——JML。简单来说,我认为JML就是一种严格的格式语言,它通过关键字来描述一个类,方法,数据对象的要求和作用。举个简单的例子:
public class IntSet {
// @ public model non_null int[] ia;
// @ ensures ia.length == 0;
public IntSet ();
/* @ assignable ia
@ ensures (\exists int j; 0 <= j && j < ia.length; ia[j] == x);
@ */
public void insert (int x);
/* @ assignable ia
@ ensures (\forall int j; 0 <= j && j < ia.length; ia[j] != x);
@ */
public void delete (int x);
/* @ ensures \result == (\exist int i; 0 <= i && i < ia.length; ia[i] == x);
@ */
public /* @ pure @ */ boolean isIn (int x);
}
上面的注释中的内容就是JML语言,其中大部分内容的介绍都能在指导书中得到解答,如前置条件requires,后置条件ensures,纯方法标识pure等。JML通过格式化的语言将原本可能含有歧义的自然语言对项目代码的要求转换为了严格的格式要求,从而能够提供更清晰的数据规范和需求。
由于本章三次作业之间的迭代内容较为简单,因此将三次迭代一同介绍。这一单元我们所需要实现的是一个模拟的社交网络,并实现社交网络中的用户,关系,消息通信等。
//社交网络类,全局只存在一个实例
public class MyNetwork implement Network;
//成员变量JML
/*@ public instance model non_null Person[] persons;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
//实现
private HashMap<Integer, MyPerson> persons;
private HashMap<Integer, Message> messages;
private HashMap<Integer, Integer> emojiMap; //key: emojiId, value: heat
private HashMap<Integer, HashSet<Message>> emojiUse;//key: emojiId, value: messages
//用户类
public class MyPerson implements Person;
//成员变量JML
/*@ public instance model int id;
@ public instance model non_null String name;
@ public instance model int age;
@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;
@ public instance model non_null Tag[] tags;
@ public instance model int money;
@ public instance model int socialValue;
@ public instance model non_null Message[] messages;
@*/
//实现
private int id;
private String name;
private int age;
private HashMap<Integer, MyPerson> acquaintance;
private HashMap<MyPerson, Integer> value;
private HashMap<Integer, MyTag> tags;
private int socialValue;
private int money;
private LinkedList<Message> messageList;
//标签类
//成员变量JML
/*@ public instance model int id;
@ public instance model non_null Person[] persons;
@*/
//实现
private int id;
private HashMap<Integer, Person> persons;
此外,还有4类message和多个异常类,在此不多赘述。
其中,层次关系为MyNetwork类中包含该网络中的所有用户MyPerson,所有待发送的message,所有已存储的emoji。每个用户MyPerson包含熟人acquaintance,好友标签MyTag以及收到的所有message。
这一部分主要分析一下作业需求的几个关键方法的复杂度
//两个用户之间的value,在Person中采用Hashmap进行存储因此复杂度为O(1)
public int queryValue(int id1, int id2);
//判断两个用户是否在同一联通分支,通过实现并查集算法因此只需要检查根节点,复杂度为O(1)
public boolean isCircle(int id1, int id2);
//统计联通分支个数,通过统计并查集根节点个数,复杂度为O(n)
public int queryBlockSum();
//统计三角关系,通过在常规操作中进行动态维护,因此复杂度为O(1)
public int queryTripleSum();
//获取最佳熟人,通过脏位进行维护,常规复杂度为O(1),脏位被设置时复杂度为O(n)
public int queryBestAcquaintance(int id);
//获取拍档个数,通过脏位进行维护,常规复杂度为O(1),脏位被设置时复杂度为O(n)
public int queryCoupleSum();
//获取最短路径,采用双边BFS,复杂度为O(V+E)
public int queryShortestPath(int id1, int id2);
//删除低活跃度Emoji,通过双Hashmap结构存储,复杂度为O(n)
public int deleteColdEmoji(int limit);
下面分别以queryTripleSum和queryCoupleSum来介绍动态维护和脏位维护
对于tripleSum,容易发现只有在进行addRelation和modifyRelation时可能会发生改变,因此,我们全局维护一个变量triNum,在relation发生变化时,当满足指定条件时就修改triNum,从而在queryTripleSum时就只需要访问triNum即可以O(1)复杂度来获取。
对于coupleSum,容易发现只有发生某用户的bestAcquaintance发生改变时才有可能改变,因此绝大部分的网络更新都不会导致coupleSum发生变化,因此采用脏位维护有很高的性价比。具体而言,在全局维护变量coupleSum和coupleDirty两个变量,每当发生用户的bestAcquaintance变更时,就对coupleDirty置位,表明此时coupleSum不可用。而每当coupleSum被通过循环重新统计一次,就清除coupleDirty,表明coupleSum可用,这样就实现了大部分情况下以O(1)复杂度获取coupleSum,少数情况下以O(n)复杂度获取。
这一原则是JML这一单元的基本原则,含义是JML所规定的数据规格和开发者的具体实现是分离的。具体到本单元而言,举以下示例
//数据规格
/*@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@ */
//实现
private HashMap<Integer, Integer> emojiMap; //key: emojiId, value: heat
//在这里, JML本身要求实现两个整型容器, 处于查找的效率要求, 在具体实现时通过一个Hashmap来实现
//方法规格
/*@ public normal_behavior
@ requires containsPerson(id1) &&
@ containsPerson(id2);
@ assignable \nothing;
@ ensures \result == (\exists Person[] array; array.length >= 2;
@ array[0].equals(getPerson(id1)) &&
@ array[array.length - 1].equals(getPerson(id2)) &&
@ (\forall int i; 0 <= i && i < array.length - 1;
@ array[i].isLinked(array[i + 1])));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !containsPerson(id1);
@ signals (PersonIdNotFoundException e) containsPerson(id1) &&
@ !containsPerson(id2);
@*/
//实现
public boolean isCircle(int id1, int id2) throws MyPersonIdNotFoundException {
if (!containsPerson(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (!containsPerson(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
if (id1 == id2) {
return true;
}
return disjoint.dsuFind(id1) == disjoint.dsuFind(id2);
}
//在实现时, 通过额外的数据结构——并查集来实现复杂度为O(1)的查找方式
对于规格与实现相分离,这一方面保证了规格建立者的最少工作,即只需要提供要求即可,另一方面给予了开发者优化效率的最大空间,即只要满足规格要求,能够采用尽可能最优的实现方式来实现规格。
在本地和互测时都需要我们进行数据构造,其中本单元的关键在于正确性测试和性能测试。正确性测试需要我们对各种功能进行对拍测试,关键在于性能测试。在中测和强测后,可以发现复杂度低于**O(n2)**的方法可以通过压测,因此我们要尽可能降低高复杂度的方法。
本单元要求我们面向JML编写指定方法的JUnit测试。我主要采用随机数据生成-克隆网络结构-JML后置条件判断的测试链来进行JUnit测试,举出如下示例
public void queryCoupleSum() throws PersonIdNotFoundException,
EqualRelationException, EqualPersonIdException, AcquaintanceNotFoundException {
Random random = new Random();
//10组测试
for (int i = 0; i < 10; i++) {
//网络建立
MyNetwork myNetwork = new MyNetwork();
MyNetwork shadow = new MyNetwork();
//双向边集存储
HashMap<Integer, HashSet<Integer>> edges = new HashMap<>();
HashSet<Integer> hasAc = new HashSet<>();
//生成Person1-100 分别加入备份队列以及network
for (int j = 1; j <= 100; j++) {
MyPerson myPerson = new MyPerson(j, "test-" + j, j);
MyPerson myPerson1 = new MyPerson(j, "test-" + j, j);
myNetwork.addPerson(myPerson);
shadow.addPerson(myPerson1);
}
//随机生成500条边 删去重复的之后加入双向边集和network
for (int j = 0; j < i * 50; j++) {
int id1 = random.nextInt(99) + 1;
int id2 = random.nextInt(99) + 1;
hasAc.add(id1);
hasAc.add(id2);
if (myNetwork.getPerson(id1).isLinked(myNetwork.getPerson(id2))) {
continue;
} else {
if (!edges.containsKey(id1)) {
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(id2);
edges.put(id1, hashSet);
} else {
edges.get(id1).add(id2);
}
int value = random.nextInt(10);
myNetwork.addRelation(id1, id2, value);
shadow.addRelation(id1, id2, value);
}
}
//函数调用
int stdout = 0;
for (Integer id : hasAc) {
int bestId = myNetwork.queryBestAcquaintance(id);
if (myNetwork.queryBestAcquaintance(bestId) == id) {
stdout++;
}
}
stdout /= 2;
int judge = myNetwork.queryCoupleSum();
//方法正确性判断
boolean result = stdout == judge;
assertTrue(result);
//pure限制判断
boolean size = myNetwork.getPersons().length == shadow.getPersons().length;
assertTrue(size);
for (int j = 0; j < myNetwork.getPersons().length; j++) {
MyPerson myPerson = (MyPerson) myNetwork.getPersons()[j];
assertTrue(myPerson.strictEquals(shadow.getPersons()[j]));
}
}
}
通过这样的测试链既能够保证数据测试充分也能保证对JML的后置条件进行充分测试。
本单元课程组主要讲解了一种冷门的规格语言JML,这样一种语言或许在我们日后的工作中不会有太大作用,但是JML为我们提供了一种规范化项目开发和设计的方法,即通过格式化的规范语言来说明提供和需求,而开发时通过这一严格的要求来给出规范的实现。