BUAA_OO Unit3 单元总结
BUAA-OO Unit3单元总结
JML简介
JML(Java Modeling Language) 是用于对 Java 程序进行规格化设计的一种表示语言。
JML规格框架:
- requires 子句定义该方法的前置条件(pre-condition);
- 副作用范围限定, assignable 列出这个方法能够修改的类成员属性, \nothing 是个关键词,表示这个方法不对任何成员属性进行修改。
- ensures 子句定义了后置条件。
原子表达式:
- \result 表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
- \old(expr) 表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
- \not_assigned(x,y,...) 表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false 。实际上,该表达式主要用于后置条件的约束表示上,即限制一个方法的实现不能对列表中的变量进行赋值。
量化表达式:
- \forall 表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
- \exists 表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
- \sum 表达式:返回给定范围内的表达式的和。
- \product 表达式:返回给定范围内的表达式的连乘结果。
- \max 表达式:返回给定范围内的表达式的最大值。
- \min 表达式:返回给定范围内的表达式的最小值。
- \num_of 表达式:返回指定变量中满足相应条件的取值个数。
集合表达式
集合构造表达式。
操作符
- 子类型关系操作符: E1<:E2 ,如果类型 E1 是类型 E2 的子类型 (sub type),则该表达式的结果为真,否则为假。如果 E1 和 E2 是相同的类型,该表达式的结果也为真;
- 等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中 b_expr1 和b_expr2 都是布尔表达式;
- 推理操作符: b_expr1= =>b_expr2 或者 b_expr2<= =b_expr1 。
第一次作业
作业简介
实现 person 类和简单社交关系的模拟和查询,学习目标为 JML 规格入门级的理解和代码实现。
容器选择
Person:
观察JML规格,发现accquaintance的id和value是一一对应的,因此可以用HahsMap来存储键值对。
Network:
考虑到在之后的方法中需要对Person进行频繁查找,因此采用<PersonId,Person>的键值对来存储。
实现思路
第一次作业较为简单,直接按照JML规格写出对应方法即可。
度量分析
在原始版本中,如果直接按照JML规格实现,那么isCircle和queryBlockSum方法占用了较高的时间复杂度(我记不清了但是大概有O(n2)或者O(n3)的样子);其他方法的复杂度正常。
bug分析
本次作业中中测和强测中没有出现Bug,互测中由于上述时间复杂度问题被hack了三刀(%……&%……*%¥#,我是真没想到第一次作业都要Hack算法淦,只能说要保持警惕啊)。
所以后来为了降低复杂度,用并查集重新写了一遍这俩方法,具体实现为:
在Network中,实现HashMap<id,father_id>存储每个用户和其祖先的id。在addPerson方法中,新增id对应的键值对,将father_id设为自己。在addRelation方法中,合并两个人的father_id。另实现getRoot方法,路径压缩+返回祖先。在isCircle方法中,对两个id分别find出祖先,以判断二人是否连通。
getRoot方法实现如下:
public int getRoot(int id) {
if (tree.get(id) == id) {
return id;
} else {
tree.put(id, getRoot(getRoot(tree.get(id))));
return tree.get(id);
}
}
第二次作业
作业简介
本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。
容器选择
Person:
新增socialValue和messages,前者直接纳入,后者选择使用ArrayList存储。
Network:
新增的group和messages都用HashMap存储。
Gruop:
未变。
Message:
不需要容器。
实现思路
本次作业新增加了Group类和Message类,Message类的方法较为简单,直接按照JML规格即可,而Group类中的getAgeMean,getAgeVar和getValueSum几个方法需要注意,(吸取了上次翻车的经验发现)直接按照JML又会出现O(n^2)以上的负责度,因此需要更改,具体方法为:
//在类中增加属性便于全局计数
private int valueSum;
private int ageSum;
//在addPerson时,更新valueSum和ageSum
@Override
public void addPerson(Person person) {
people.put(person.getId(), (MyPerson) person);
ageSum += person.getAge();
for (MyPerson p : people.values()) {
// renew valueSum
if (p.isLinked(person)) {
valueSum += (p.queryValue(person)) * 2;
}
}
}
//同理,deletePerson时也需要更新
@Override
public void delPerson(Person person) {
people.remove(person.getId());
ageSum -= person.getAge();
for (MyPerson p : people.values()) {
// renew valueSum
if (p.isLinked(person)) {
valueSum -= (p.queryValue(person)) * 2;
}
}
}
//有前述内容铺垫的基础上,这三个方法的复杂度即可被压缩
@Override
public int getValueSum() {
return valueSum;
}
@Override
public int getAgeMean() {
if (getSize() == 0) {
return 0;
}
return ageSum / getSize();
}
@Override
public int getAgeVar() {
int ageVar = 0;
int size = getSize();
if (size == 0) {
return 0;
}
for (MyPerson p : people.values()) {
ageVar += (p.getAge() - getAgeMean()) * (p.getAge() - getAgeMean());
}
return ageVar / size;
}
记得addPerson和deletePerson两个方法中都要更新这两个属性。
度量分析
由于把valueSum和ageSum在全局中更新而不是每次调用时遍历,因此方法整体的时间复杂度较为合理。
bug分析
本次作业强测大翻车,问题在于看错了JML的规格:
//JML规格如下:
/*@ ensures \result == (people.length == 0? 0 :
@ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
@*/
public /*@pure@*/ int getAgeMean();
可以发现,按照括号的层次,应该是先对people中的age进行求和,再除以people的长度;而如果先除以people的长度再求和,则可能因为int/int造成的数据精度缺失。
阅读JML一定要仔细www
第三次作业
作业简介
本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。
容器选择
Network:
由于emojiId和emojHeat可以组成键值对,因此只需要一个HashMap即可存放这两个值。
实现思路
在Message的基础上,延展出了三个子类:EmojiMessage、RedenvelopMessage、NoticeMessage,较为简单,直接按照JML规格实现即可。
本次作业的难点和重点在于Network的sengIndirectMessage方法,本质即是求两点间最短路径。查阅资料后,发现有弗洛伊德算法和迪杰斯特拉算法可考虑。前者可以求从任一结点到另一结点的路径;而后者只能求node[0]到其他所有结点的路径。但考虑到前者是O(n3)的时间复杂度而后者仅为O(n2),因此最终还是选择迪杰斯特拉算法,实现如下:
首先为便于比较,新建一个Pair类,存储了id和value,进而实现一个新的comparator。
public class Pair {
private int id;
private int dis;
public Pair(int id, int dis) {
this.id = id;
this.dis = dis;
}
public int getId() {
return this.id;
}
public int getDis() {
return this.dis;
}
public void setDis(int dis) {
this.dis = dis;
}
}
首先要判断两个结点是否连通:
- 如果两点不连通(即
!isCircle(id1,id2)),直接返回-1 - 反之,如果两点相同,返回0
- 以上两者都不符合,使用迪杰斯特拉算法寻找最小值。
@Override
public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
//求最短路径且返回路径长度。
if (messages.containsKey(id) && getMessage(id).getType() == 0) {
Message m = getMessage(id);
int value = m.getSocialValue();
MyPerson p1 = (MyPerson) m.getPerson1();
MyPerson p2 = (MyPerson) m.getPerson2();
if (!checkIsCircle(p1.getId(), p2.getId())) {
return -1;
} else if (p1.equals(p2)) {
return 0;
}
p1.addSocialValue(value);
p2.addSocialValue(value);
if (m instanceof RedEnvelopeMessage) {
int money = ((RedEnvelopeMessage) m).getMoney();
p1.subMoney(money);
p2.addMoney(money);
} else if (m instanceof EmojiMessage) {
int heat = emojiHeatList.get(((EmojiMessage) m).getEmojiId()) + 1;
emojiHeatList.replace(((EmojiMessage) m).getEmojiId(), heat);
}
p2.getMessages().add(0, m);
messages.remove(id);
return dijkstra(p1.getId(), p2.getId());
} else {
throw new MyMessageIdNotFoundException(id);
}
}
由于我采用的是HashMap来存储Messages,不方便直接建立图结构,因此还是将其转化为ArrayList后使用算法,实现如下:
private static Comparator<Pair> pairComparator = Comparator.comparingInt(Pair::getDis);
private int dijkstra(int id1, int id2) {
HashMap<Integer, Pair> paths = new HashMap<>(5000);//记录每个点到id1的长度
ArrayList<Integer> used = new ArrayList<>(5000);//记录用过的点
used.add(id1);
for (Integer i : people.keySet()) {
paths.put(i, new Pair(i, Integer.MAX_VALUE));
}
paths.get(id1).setDis(0);
Queue<Pair> remains = new PriorityQueue<>(5000, pairComparator);
remains.add(paths.get(id1));
Pair vertex = remains.poll();
while (true) {
Person currentPerson = getPerson(vertex.getId());
for (Map.Entry<Integer, Integer> entry :
((MyPerson) currentPerson).getAcquaintance().entrySet()) {
int id = entry.getKey();
if (!used.contains(id)) {
Person person = getPerson(id);
int dis = currentPerson.queryValue(person) + paths.get(vertex.getId()).getDis();
if (dis < paths.get(id).getDis()) {
paths.get(id).setDis(dis);
remains.remove(paths.get(id));
remains.add(paths.get(id));
}
}
}
vertex = remains.poll();
if (vertex.getId() == id2) {
return paths.get(vertex.getId()).getDis();
}
used.add(vertex.getId());
}
度量分析
使用迪杰斯特拉算法后,时间复杂度最高的方法即为上述方法,O(n^2),但由于数据范围限制,最终也被控制在较为可观的范围里。
bug分析
本次大胜利,强测互测都没有被hack到。反思原因,首先方法的时间复杂度还算过得去很重要,其次和同学对拍多次,(也是在对拍的过程中发现了许多bug)。
关于测试方法,只有第一次作业使用了Junit方法以检查规范性,后面两次还是觉得对拍效率更高。
感想
相较于前两单元的作业,本次作业真的可以说是送分。
JML不难,在简单阅读并理解范例后可以很快入门,但是随着代码量的增多和复杂度的增加,JML规格变得越来越复杂,其中包含的信息也越来越多,稍有不慎就可能看漏或者看错其中的一些细节(第二次作业大翻车就是很好的体现),因此更为仔细的阅读是JML学习中必不可少的。
此外,通过第一次被hack的教训我也意识到,JML更多体现的是最终需要达到的“结果”,中间的过程、如何实现需要我们自己思考。以这一单元的几个方法为例,如果单纯按照JML给出的思路去写,很有可能会TLE,对其进行优化,也就更显得必要。
总之,本单元虽然简单却仍有其精妙之处,学到了很多东西,值得!

浙公网安备 33010602011771号