OO第三单元总结
BUAA_OO_2022_第三单元总结
OO第三单元作业主题为JML规格,具体的任务是实现简单社交关系的模拟和查询,阅读官方包接口中方法的JML规格之后在自己创建的类中实现,下面我分享一下我在本单元的学习心得和作业完成情况。
第一次作业
架构设计
在第一次作业中,我们需要自己建立MyGroup,MyNetwork以及MyPerson这三个类以及六个异常,由于是第三单元的第一次作业,因此本次作业涉及的大部分方法都是非常基础的方法,我们也不需要额外的添加类。本次作业的一大难点还是对于JML规格的理解,因为有些时候我们的主观理解会与实际的JML规格产生一定出入(比如说isCircle方法我一开始一直以为是判断是否成环),当我们看懂JML规格并严格按照JML规格编写代码,那本次作业的正确性就不成问题了。
性能问题和修复情况
在本次作业中并没有涉及到性能问题的方法,即使是isCircle方法,使用dfs和bfs也不会遇到性能问题,但在本方法中我采用了并查集的优化,具体实现如下:
//in MyPerson
private int father = -1;
//in MyNetwork
public int root(int id) {
MyPerson person = (MyPerson) getPerson(id);
while (person.getFather() != -1) {
person = (MyPerson) getPerson(person.getFather());
}
return person.getId();
}
public void addRelation(int id1, int id2, int value) {
//...
int root1 = root(person1.getId());
int root2 = root(person2.getId());
if (root1 != root2) {
MyPerson rootPerson1 = (MyPerson) getPerson(root1);
MyPerson rootPerson2 = (MyPerson) getPerson(root2);
rootPerson1.setFather(rootPerson2);
}
}
public boolean isCircle(int id1, int id2) {
int root1 = root(id1);
int root2 = root(id2);
return root1 == root2;
}
bug修复
本单元中我在强测和互测中都没有出现bug,同时我也没有hack到人,但我的代码存在一个严重的问题:father默认为-1,这导致只要出现一个id为-1的person就会使我的程序产生错误。
第二次作业
架构设计
在本次作业中官方包新增了Message接口和两个异常,因此在作业中也需要新增MyMessage类和自己实现的两个异常,同时,为了实现kruskal算法,在本次作业中我额外新增了MyLine类,存储整个关系图的边信息,同时实现Comparable接口使得其支持sort排序,代码如下:
public class MyLine implements Comparable<MyLine> {
private int start;
private int end;
private int value;
//here are some "get" methods
@Override
public int compareTo(MyLine o) {
if (this.value < o.getValue()) {
return -1;
}
else if (this.value > o.getValue()) {
return 1;
}
else {
return 0;
}
}
}
性能问题和修复情况
在上面也提到,本次作业的qlc指令涉及了最小生成树算法,但kruskal算法如果不进行优化,显然是性能不够的(On^2),因此我们需要对其进行并查集优化,但显然,并查集不进行优化的话最坏情况的时间复杂度是On,这时我们就需要对并查集进行优化,我选用的是助教之前在第一次作业讨论区提供的一种优化,即“小树接在大树下”,在实际实现中,我给MyPerson类里增加了一个depth属性来实现:
//in MyNetWork
public void addRelation(int id1, int id2, int value) {
//...
int root1 = root(person1.getId());
int root2 = root(person2.getId());
if (root1 != root2) {
MyPerson rootPerson1 = (MyPerson) getPerson(root1);
MyPerson rootPerson2 = (MyPerson) getPerson(root2);
if (rootPerson1.getDepth() < rootPerson2.getDepth()) {
rootPerson1.setFather(rootPerson2);
}
else if (rootPerson1.getDepth() > rootPerson2.getDepth()) {
rootPerson2.setFather(rootPerson1);
}
else {
rootPerson1.setFather(rootPerson2);
rootPerson2.addDepth(1);
}
}
}
同时,第一次作业中MyGroup中的getValueSum和getAgeVar方法时间复杂度为On^2,在本次作业中会遇到性能问题,因此本次作业我将ValueSum和AgeMean的值维护起来,这样两个方法的时间复杂度就下降到了O1和On。
bug修复
本次作业我在强测没有出错,但由于我之前提到的father默认为-1的bug没有被修复,因此互测被hack了四次。在互测中我发现了一名同学异常顺序错误从而成功hack一刀。
第三次作业
架构设计
在本次作业中,我们引入了三类Message和额外的两个异常。在本次作业中我们很有可能会遇到一个比较严重的问题,就是MyNetwork的行数会超过500行导致代码风格分雪崩,这个时候我们需要单独开一个或者数个工具类去“代替”MyNetwork类执行工作。我在本次作业中就额外开了一个MyDijkstra类来实现sim指令中涉及的dijsktra算法。
性能问题和修复情况
在本次作业中产生的性能问题就是dijsktra算法,我们需要将其进行堆优化使得我们程序的性能可以满足本次作业要求,以下是我在MyDijkstra中实现的方法:
public class MyDijkstra {
class Edge {
private final int to;
private final int len;
Edge(int to, int len) {
this.to = to;
this.len = len;
}
}
private HashMap<Integer, Boolean> visited = new HashMap<>();
private HashMap<Integer, Integer> distance = new HashMap<>();
public MyDijkstra(HashMap<Integer, Person> people, Person fromPerson) {
PriorityQueue<Edge> queue = new PriorityQueue<Edge>(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((Edge) o1).len - ((Edge) o2).len;
}
});
queue.offer(new Edge(fromPerson.getId(), 0));
for (Person person : people.values()) {
visited.put(person.getId(), false);
distance.put(person.getId(), Integer.MAX_VALUE);
}
distance.put(fromPerson.getId(), 0);
while (!queue.isEmpty()) {
int toId = queue.poll().to;
if (visited.get(toId)) {
continue;
}
visited.put(toId, true);
for (Integer acquaintanceId : ((MyPerson)people.get(toId)).getAcquaintance().keySet()) {
int id = acquaintanceId;
int value = ((MyPerson)people.get(toId)).getValue().get(acquaintanceId);
if (distance.get(id) > distance.get(toId) + value) {
distance.put(id, distance.get(toId) + value);
queue.offer(new Edge(id, distance.get(id)));
}
}
}
}
}
bug修复
本次作业我在强测中没有bug,但本次作业有一个极其隐蔽的坑点:对于Person,其存储的所有Message的id不保证互不相同,因此当你调用List.remove方法时,它会用equal方法去删除元素,而我们将equal方法重写为了仅比较id,因此此时会产生错删现象,我也因此被hack了。
测试方法
本次作业我的测试方法主要是对拍,我和同学合作的评测机的数据构造模块可以实现自主设置触发各类异常的概率(比如idNotFound这类异常就是设置参数id在Network中不存在的概率),从而可以全方位测试所有方法和异常,但由于数据是随机构造,对于特定数据和边缘数据的测试是很弱的,我本单元的两个Bug都是随机数据难以测试到的bug。
扩展Network
假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
首先,我们将Advertiser、Producer、Customer继承Person类,之后,我们建立AdMessage,ProductMessage和buyMessage类并继承Message类。
为了存储所有商品,我们建立一个名为product的HashMap,以MessageId为key,value为ProductMessage。
为了实现查询销售额,我们建立一个名为productValue的HashMap,以MessageId为key,value为Integer类型的销售额。
为了实现查询销售路径,我们将ProductMessage中加入producerId和BuyerId。
在Network中添加的方法如下:
boolean containsProduct(int productId);
void addProduct(int id, int productId);
int getProductValue(int productId);
void buyProduct(int productId, int buyerId, int producerId);
List<Integer> getRoute(int productId);
具体JML规格如下:
//in NetWork
/*@ public normal_behavior
@requires contains(id) && !containsProduct(productId);
@assignable products;
@ensures products.length = \old(products.length) + 1;
@ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length;
@ products[j] == (\old(products[i]))));
@ ensures (\exists int i; 0 <= i && i < products.length;
@ products[i].getId() == product);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id);
@ signals (EqualMessageIdException e) containsProduct(productId);
@*/
void addProduct(int id, int productId) throws PersonIdNotFoundException, EqualProductIdException
/*@ public normal_behavior
@ requires containsProduct(productId);
@ ensures (\exists int i; 0 <= i && i < product.length;
@ product[i].getId() == productId && \result == productValue[i]);
@ also
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !containsProduct(productId);
@*/
int getProductValue(int productId) throws ProductIdNotFoundException
/*@ public normal_behavior
@ requires containsProduct(productId);
@ assignable productValue;
@ assignable product[*].buyerId;
@ assignable product[*].producerId;
@ ensures (\exists int i; 0 <= i && i < product.length;
@ product[i].getId() == productId && productValue[i] == \old(productValue[i]) + 1 &&
@ product[i].buyerId == buyerId && product[i].producerId == producerId);
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsProduct(productId);
@*/
void buyProduct(int productId, int buyerId, int producerId) throws ProductIdNotFoundException
心得体会
在本单元的学习中我初步理解了JML规格的阅读。
第三单元应该是整个oo里最简单的一个单元,因此本单元的代码编写并没有遇到太多困难,但本单元的坑点有很多,因此我在检查正确性的时候往往是反复比对自己写的代码和jml规格来确认正确性,虽然最后仍然犯下了一些小错但还好强测全部通过了。