BUAA_OO_第三单元作业总结
BUAA_OO_第三单元作业总结
总体概述
本单元的任务为实现一个简单的社交网络,分三次逐步迭代使社交网络具有分组,收发消息,计算群组内相关信息等相关功能。本次作业旨在训练阅读JML规格,以及具备根据JML给出的规格编写Java代码的能力,了解JML规格在面向对象设计与构造中的重要意义,并掌握利用JML规格提高代码质量的能力。与此同时,本次作业也考察了同学们对图论知识的掌握情况,要求在读懂JML的同时选用合适的图论算法满足性能需求,提高程序质量。
第九次作业
1、题目概述
本次作业,需要完成的任务为实现简单社交关系的模拟和查询,学习目标为 入门级JML规格理解与代码实现。通过阅读官方包的JML规格来实现Person
、Group
、Network
接口,实现自己的社交网络,需要准确理解 JML 规格并保证代码实现严格符合对应的 JML 规格。
2、类图
3、作业思路
容器选择
因为本次作业需要大量对Person
、Group
的查询,因此我大量使用了HashMap
来进行存储,除此之外选择了并查集来存储连通块信息,提高连通块相关的查询效率。
图架构
本单元作业背景是社交网络模型,Person
可以抽象为图中的点,Relation
可以抽象为图中的边,Group
可以抽象为点集。
算法选择
图论算法
本次作业需要查询点之间的连通性以及连通块的数量,因此选用进行路径压缩优化的并查集维护连通关系,提高查询效率。
public class UnionFindSet {
private HashMap<Integer, Integer> parent;
private int unionNum;//维护连通块的数量,可通过o(1)查询
public UnionFindSet() {
parent = new HashMap<Integer, Integer>();
unionNum = 0;
}
public void insert(int id) {
parent.put(id, id);
unionNum++;
}
private int getFather(int id) {
int root = id;
while (true) {
int father = parent.get(root);
if (father == root) {
break;
}
root = father;
}
int now = id;
//路径压缩,可将查找效率优化到o(1)
while (true) {
int father = parent.get(now);
if (father == now) {
break;
}
parent.put(now, root);
now = father;
}
return root;
}
public boolean inUnion(int id1, int id2) {
int f1 = getFather(id1);
int f2 = getFather(id2);
return f1 == f2;
}
public void link(int id1, int id2) {
int f1 = getFather(id1);
int f2 = getFather(id2);
if (f1 == f2) {
return;
}
unionNum--;
parent.put(f1, f2);
}
public int getUnionNum() {
return unionNum;
}
}
数据结构算法
在Group
中维护了ageSum
、ageSumSq
、valueSum
来提高相关指令的查询效率
4、测试环节与Bug分析
测试
本单元我们学习了运用JUnit来进行测试。JUnit的核心是使用assert检验JML里的后置条件和不变式是否满足。大致思路是在需要测试的方法中,先调用初始化的方法来建立测试所需要的环境,然后调用待测试的方法,并使用assert来检验该方法运行是否正确。除此之外,我还选择了与同学进行随机大数据对拍来进行测试,保证程序没有正确性错误。
Bug分析
因为作业有JML规格进行约束,只要是严格遵循JML的要求来实现接口就不会发生正确性错误,这也是JML规格辅助程序设计的优越之处。因此本次作业无论是我自己的程序还是互测环节遇到其他同学的程序都基本没有结果正确性问题。然而因为本次作业有性能要求,因此需要利用算法降低时间复杂度,否则在强测与互测环节可能会出现TLE的问题。本次作业我在使用并查集时忘记维护连通分支的数量了,因此在互测中被大量的qbs
测试导致出现了TLE,同时也用同类型的数据hack了其他同学。
5、总结
本次作业是对JML规格的初步接触,旨在了解JML规格,并尝试使用JUnit来进行测试。因为本单元初次接触到JML规格,指导书也没有对任务要求实现功能的解读,因此刚开始完全不知道本次作业的目标到底是个什么东西,一开始真的有些头疼,不过好在通读完所有JML后开始熟悉阅读JML的方法了,在理解JML规格后根据规格实现接口还是相对容易的。
第十次作业
1、题目概述
本次作业,需要完成的目标是进一步实现社交关系模拟系统中的群组和消息功能,学习目标为进一步掌握JML规格的理解与实现。
2、类图
3、作业思路
图架构
相比上次作业,本次作业需要实现最小生成树,因此用Relation
类保存了边的关系,在求最小生成树时遍历所有边来获取连通分支的点集,并在最小生成树算法中计算权值。
算法选择
在上次作业时已经实现了并查集算法,因此本次作业选用了利用并查集优化的Kruskal算法来计算最小生成树。
public class Kruskal {
private UnionFindSet ufs;
private ArrayList<Relation> relations;
private HashSet<Integer> allPeople;
private int id;
private int leastSum;
public Kruskal(int id) {
ufs = new UnionFindSet();
relations = new ArrayList<>();
allPeople = new HashSet<Integer>();
this.id = id;
leastSum = 0;
allPeople.add(id);
ufs.insert(id);
}
//遍历所有边加入该连通分支中的点与边
public void addRelation(Relation relation) {
relations.add(relation);
int id1 = relation.getId1();
int id2 = relation.getId2();
if (!allPeople.contains(id1)) {
allPeople.add(id1);
ufs.insert(id1);
}
if (!allPeople.contains(id2)) {
allPeople.add(id2);
ufs.insert(id2);
}
}
//求值
public int getLeastSum() {
Collections.sort(relations);
for (Relation relation : relations) {
if (ufs.getUnionNum() == 1) {
break;
}
if (!ufs.inUnion(relation.getId1(), relation.getId2())) {
ufs.link(relation.getId1(), relation.getId2());
leastSum += relation.getValue();
}
}
return leastSum;
}
}
4、测试环节与Bug分析
本次作业的测试策略与上次作业相同。本次作业因为算法选取较好,能较好的满足性能需求,因此没有被找出bug,在互测阶段,发现有人没能够维护AgeVar
、和ValueSum
,导致查询的时间复杂度为o(n²),因此针对构造数据hack了3个同学。
5、总结
本次作业进一步实现社交关系模拟系统中的群组和消息功能,加深了我们对JML规格的掌握与理解,同时加大了图论算法的要求。这次作业sendMessage
和addMessage
这两个方法的JML规格非常长,读完这两个规格对JML的阅读能力又上了一个台阶。
第十一次作业
1、作业概述
本次作业,需要完成的目标是进一步实现社交关系系统中不同消息类型以及相关操作,学习目标是理解JML规格在面向对象设计与构造中的重要意义,并掌握利用JML规格提高代码质量的能力。
2、类图
3、作业思路
本次作业图架构沿用前两次架构,新增了两种特殊Message
,EmojiMessage
与NoticeMessage
。本次作业需要实现最短路径算法,因此我选用了PriorityQueue
作为容器实现小顶堆来实现堆优化的Dijkstra
算法。
public class Dijkstra {
private MyPerson person1;
private MyPerson person2;
private PriorityQueue<Distance> pq;
private HashMap<Integer, Integer> minDis;
private HashSet<Integer> visit;
private static int INF = 0x7fffffff;
public Dijkstra(MyPerson person1, MyPerson person2) {
this.person1 = person1;
this.person2 = person2;
pq = new PriorityQueue<Distance>();
minDis = new HashMap<Integer, Integer>();
visit = new HashSet<>();
}
public int minDistance() {
minDis.put(person1.getId(), 0);
pq.add(new Distance(person1, 0)); //将起始的距离初始化为0
while (!pq.isEmpty()) {
Distance dis = pq.poll();//取出并弹出堆顶
MyPerson person = dis.getPerson();
int distance = dis.getDistance();
if (person.equals(person2)) { //已经访问到person2
return distance;
}
if (visit.contains(person.getId())) {
continue;
}
visit.add(person.getId());//设置标记
HashMap<Integer, Person> linkPerson = person.getAcquaintance();
for (Person next : linkPerson.values()) {
int oriDistance = getDistance(next.getId());
if (oriDistance > distance + person.queryValue(next)) { //更新距离
int newDistance = distance + person.queryValue(next);
minDis.put(next.getId(), newDistance);
pq.add(new Distance((MyPerson) next, newDistance));
}
}
}
return -1;
}
public int getDistance(int id) {
if (minDis.containsKey(id)) {
return minDis.get(id);
}
return INF;
}
}
4、测试环节与Bug分析
本次我自己仍然没有发现Bug。由于新增了两个Message
类的子类,增加了一些复杂的计算要求,因此有些同学出现了JML实现的错误。一开始我选择用自己的程序作为基准程序用大量随机数据与房内同学对拍的方式找到了2个Bug,但要从大数据中找出造成Bug的指令并生成能提交的互测数据非常繁琐,因此后来我在czh同学的帮助下选择利用Python生成覆盖所有JML分支的强数据进行hack,效果非常好,成功hack了5个同学。本次作业大家基本没有性能问题,反倒是因为JML理解错误造成的计算错误发生率比较高。
5、总结
本次作业对JML规格的要求又上了一个台阶,从互测阶段不少同学因为规格理解错误而被找出了Bug可以看出。本次作业也是本单元最后一次作业,在这次作业上我进一步体会了JML的优点,可比较前后两次JML的区别就能明白本次作业需要迭代的内容,只要在开发过程中严格遵守JML规格,就能保证程序的正确性。同样也可使用JML规格来编写对应的测试数据,全覆盖到所有情况及逆行测试,对Bug的发现率也高。这三次作业下来我对JML规格的理解了JML规格在面向对象设计与构造中的重要意义,并掌握利用JML规格提高代码质量的能力。
NetWork拓展
题目要求
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
发送广告
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && people[i].containsAdvertisement(advertisementId));
@ assignable people[*].messages
@ ensures (\forall int i; 0 <= i && i < people.length; (getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length) + 1 && people[i].messages[0] == \old(getPerson(id).getAdvertisement(advertisementId)) && (\forall int j; 1 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j - 1]))));
@ ensures (\forall int i; 0 <= i && i < people.length; !(getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length && (\forall int j; 0 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j]))));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < people.length; people[i].getId() != id || (people[i].getId() == id && !people[i] instanceof Advertiser));
@ signals (AdvertisementIdNotFoundException e) (\exists int i; 0 <= i && i < people.length && people[i].getId() == id && people[i] instanceof Advertiser; !people[i].containsAdvertisement(advertisementId));
@*/
public void sendAdvertisement(int id, int advertisementId) throws PersonIdNotFoundException, AdvertisementIdNotFoundException;
设置偏好
/*@ public normal_behavior
@ requires contains(personId);
@ requires containsProduct(productId);
@ ensures getPerson(personId).isFavorable(productId) == true;
@*/
public /*@ pure @*/void saleProduct(int personId, int productId);
购买商品
/*@ public normal_behavior
@ requires (containsCustomer(id1) && containsProducer(id2) && num > 0 && ReadyToBuy(id1, id2));
@ assignable getProducer(id2).sales, containsCustomer(id1).money;
@ ensures getProducer(id2).sales == \old(getProducer(id2).sales) + getProducer(id2).productPrice * num;
@ ensures getCustomer(id1).money == \old(getCustomer(id1).money) - getProducer(id1).productPrice * num;
@ ensures \result == true;
@ also
@ public normal_behavior
@ requires (containsCustomer(id1) && containsProducer(id2) && num < 0 && && ReadyToBuy(id1, id2));
@ assignable \nothing;
@ ensures \result == false;
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundException e) !containsCustomer(id1));
@ signals (ProducerIdNotFoundException e) (containsCustomer(id1) && !containsProducer(id2));
@ signals (ProducerNotReadyToBuyException e) (containsCustomer(id1) && containsProducer(id2) && !ReadyToBuy(id1, id2));
@*/
public boolean buyProduct(int id1, int id2, int num) throws
CustomerIdNotFoundException, ProducerIdNotFoundException, ProducerNotReadyToBuyException;
心得体会
本单元作业以社交网路为载体,让我们了解掌握了JML规格与契约化变成的开发方式。JML规格的好处在与定义严谨,只要严格遵守规格就可以满足程序的正确性,测试也可以根据JML规格来构造针对性数据覆盖每一个分支。但JML同样也有其弊端,有些需求用JML表示起来非常复杂,例如第二次作业的qlc
指令,其本质就是一个最小生成树,但通过JML规格来描述就非常复杂,我读了很久才勉强理解。总之,对这个单元的学习我还是相对较满意的,在第四单元的作业中也同样可以应用到本单元学习的JUnit测试方法。