BUAA OO 第三单元总结
BUAA OO 第三单元总结
〇.综述
第三单元训练的主题是规格化设计。在本单元的实现过程中,笔者认为是一个“戴着镣铐跳舞”的过程:要在给定的JML的规格约束下,以灵活的数据结构实现规格给定的功能。在这个过程中,JML规格约束给出了逻辑正确性的保证,而数据结构的具体实现是程序性能的决定因素。
一.架构设计
在JML的规格约束下,大层面上的架构已被设计完备,需要我们格外关注的是存储JML中各个量的数据结构形式。下面给出笔者在三次作业中各个给定类的具体数据结构形式。
-
MyPerson类
private HashMap<Person, Integer> acquaintance; private LinkedList<Message> messages;
-
acquaintance
的key为该person的acquaintance,value为亲密值; -
messages
为用链表存储的消息。由于该类中涉及消息的头插,因此使用链表存储更加方便。
-
-
MyGroup类
private HashMap<Integer, Person> people; private int meanSum; private int squareSum; private int valueSum;
people
的key为person的id值,value为该id对应的person,依此法组织便于通过id值快速查找到对应的person;meanSum
、squareSum
、valueSum
为三个维护量,便于实现对应统计量的O(1)查询。具体的维护方式在二.性能优化部分会有详细阐述。
-
MyNetwork类
private HashMap<Integer, Person> people; private HashMap<Integer, Group> groups; private HashMap<Integer, Message> messages; private HashMap<Integer, Integer> emojiMap;
均为以id值为key的HashMap,便于利用id直接查询。
此外,笔者还额外构建了UnionFind
、Edge
、Graph
三个类便于图信息的存储与计算。下面对这三个类进行详细介绍。
-
UnionFind类
private HashMap<Integer, Integer> parent; private HashMap<Integer, Integer> depth; private int cnt;
维护了各个
person
对象的连通关系。其中parent
记录了各person
的父节点,这也是各节点是否连通的判断标志。与一般的并查集相同,核心的两个方法是
find()
与merge()
。额外地,每一次调用find()
方法时,由于仅需维护连通关系,因此会将查询过程中的各节点的父节点设置为同一值。这一过程实质上将连通树的深度压缩为1,提高了查找效率。具体实现方法如下:public int find(int p) { int fa = parent.get(p); if (fa == p) { return p; } else { int newFa = find(fa); parent.put(p, newFa); return newFa; } }
-
Edge类
public class Edge implements Comparable<Edge> { private Person from; private Person to; private int weight; ... }
实现了
Comparable
接口,以权值weight
作为比较大小的依据,便于在后面的Graph
类中实现堆优化的dijkstra算法。实现接口重写需重写compareTo()
方法:public int compareTo(Edge o) { if (this.weight < o.getWeight()) { return -1; } else if (this.weight > o.getWeight()) { return 1; } else { return 0; } }
-
Graph类
private ArrayList<Edge> edges; private UnionFind unionFind; private HashMap<Integer, Integer> dirty; private HashMap<Integer, Integer> leastConnection; private HashMap<Integer, ArrayList<Edge>> adjTable; private boolean isSorted;
核心在于
adjTable
,以key为结点、value为Edge的ArrayList的HashMap存储邻接表。邻接表的图存储形式可以方便dijkstra算法的实现。此外,注意到如果不向Network中添加新的relation,即不向Graph中添加新的边,现存各节点的
leastConnection
不会发生改变。为了在现有堆优化的dijkstra算法的基础上达到更好的时间性能,额外设置了HashMap<Integer, Integer> dirty
,利用类似OS课程中“脏位”的思想标记各结点——倘若没有与之有关的新的relation加入,则直接输出之前计算出并存储好的统计量,以便有效应对大量重复query指令的压力测试。
二.性能优化
在构建数据结构时,一方面需要考虑到程序的计算性能,即构建的数据结构可以有效降低各个查询量计算方法的时间复杂度,如引入并查集等;另一方面需要考虑到程序的维护性能,即构建的数据结构能够尽可能多的维护各个查询量,以便将更多的查询方法降到O(1)的时间复杂度。
1.计算性能
主要体现在以下几个方面:
-
选择合适的容器
如Network中的person、group等使用HashMap存储便于实现O(1)查找,Person中的message使用LinkedList存储便于头插等
-
构建合适的图结构
先后构建了记录连通信息的UnionFind类、实现了
Comparable
接口的Edge类、实现了邻接表的Graph类,便于实现堆优化的dijkstra算法 -
寻找合适的算法
主要涉及最小生成树算法与最短路径算法。由于笔者的图结构主要以边集、邻接表形式存储,因此使用了kruskal算法与堆优化的dijkstra算法
2.维护性能
尽可能地维护更多的统计量,同时结合脏位的思想标记维护量的更新状态
- ageMean、ageVar:由于涉及到整除问题,不能直接维护以上两个量。需要维护meanSum与squareSum,借助均值、方差的相关公式计算出当前的实际值即可
- isCircle、BlockSum:借助并查集维护。在实现过程中可以适当优化,如在查找过程中压缩树的高度,具体实现方法在一.架构设计中有详细说明
- sendIndirectMessage、LeastConnection:除了借助算法优化外,还额外设置了脏位,用于应对大量连续query指令的压力测试,具体实现方法在一.架构设计中有详细说明
- 此外,其余诸如peopleNum等统计量可以直接调用相关容器的
.size()
方法
在三次作业中的第二次作业中,笔者没有很好的关注到算法性能方面的问题,导致查询量valueSum
的查询方法的复杂度达到O(n²)进而被房友hack。后来通过在Group中增加维护量valueSum
并在addPerson、delPerson两个方法中更新该量,完成了性能维护。
三.规格测试
1.根据JML规格构造测试数据
不难发现,JML规格中的require
语句,正好对应着数据的范围要求。因此,为了更加全面地测试到程序的每一个分支,可以根据JML的require
语句构造对应的数据进行测试。
而测试结果的正确性,则由JML的后置语句ensure
作为判据。
由此体会到:JML规格不仅在设计层面提供了正确性的保证,更在测试层面保证了数据构造的全面性。
2.压力测试
然而,根据JML构造数据只提供了程序正确性的保证,而不能保证程序具有较好的性能。因此除了全面性的数据构造,还要考虑到大量指令的集中输入情况(如大量ap后再大量qgvs),以测试程序的性能优良。
3.随机测试
根据JML规格测试保证了程序的正确性,大量的压力测试保证了程序的性能。然而手撸数据太慢了。再辅以随机数据+对拍器进行自动评测,可以更好地对程序进行实际应用场景的测试。
四.异常处理
此外,本单元还较多地涉及到异常处理的知识。笔者简单做了如下总结:
讨论:Java异常讨论 - 第十一次作业 - 2022面向对象设计与构造 | 面向对象设计与构造 (buaa.edu.cn)
五.拓展作业
-
生产产品
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) && (getPerson(id) instanceof Producer) && getProducer(id).hasProduct(productId); @ assignable getProducer(id).productCounts; @ ensures getProducer(id).getProductCounts(productId) == @ \old(getProducer(id).getProductCounts(productId)) + 1; @ also @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) && (getPerson(id) instanceof Producer) && !getProducer(id).hasProduct(productId); @ assignable getProducer(id).products, getProducer(id).productCounts; @ ensures (\exists int i; 0 <= i && i < getProducer(id).products.length; getProducer(id).products[i] == productId && getProducer(id).productCounts[i] == 1); @ ensures getProducer(id).products.length == \old(getProducer(id).products.length) + 1 && @ getProducer(id).productCounts.length == \old(getProducer(id).productCounts.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(getProducer(id).products.length); @ (\exists int j; 0 <= j && j < getProducer(id).products.length; getProducer(id).products[j] == \old(getProducer(id).products[i]) && @ getProducer(id).productCounts[j] == \old(getProducer(id).productCounts[i]))); @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id); @ signals (NotProducerException e) (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) && !(getPerson(id) instanceof Producer); @*/ public void produceProduct(int id, int productId) throws PersonIdNotFoundException, NotProducerException;
-
发送广告
/*@ public normal_behavior @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement) && getMessage(id).getProuctId == productId; @ assignable messages; @ assignable people[*].messages; @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\forall int i; 0 <= i && i < people.length && !getMessage(id).getPerson1().isLinked(people[i]); @ people[i].getMessages().equals(\old(people[i].getMessages())); @ ensures (\forall int i; 0 <= i && i < people.length && getMessage(id).getPerson1().isLinked(people[i]); @ (\forall int j; 0 <= j && j < \old(people[i].getMessages().size()); @ people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) && @ people[i].getMessages().get(0).equals(\old(getMessage(id))) && @ people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1); @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); @ signals (NotAdvertisementException e) containsMessage(id) && !(getMessage(id) instanceof Advertisement); @ signals (WrongAdvertisementException e) containsMessage(id) && (getMessage(id) instanceof Advertisement) && !(getMessage(id).getProuctId == productId); @*/ public void sendAdvertisement(int id, int productId) throws MessageIdNotFoundException, NotAdvertisementException, WrongAdvertisementException;
-
添加商品
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id) && (getPerson(id) instanceof Customer) && !getCust(id).hasProduct(productId); @ assignable getCust(id).products; @ ensures (\forall Product i; \old(getCust(id).hasProduct(i)); @ getCust(id).hasProduct(i)); @ ensures \old(getCust(id).products.length) == getCust(id).products.length - 1; @ ensures getCust(id).hasProduct(getProduct(productId)); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; @ people[i].getId() == id); @ signals (NotCustomerException e) (\exists int i; 0 <= i && i < people.length; @ people[i].getId() == id) && !(getPerson(id) instanceof Customer); @ signals (EqualProductException e) (\exists int i; 0 <= i && i < people.length; @ people[i].getId() == id) && (getPerson(id) instanceof Customer) && getCust(id).hasProduct(productId); */ public void setPreference(int id, int productId) throws PersonIdNotFoundException, NotCustomerException, EqualProductException e;
六.心得体会
众所周知,第三单元是面向算法的设计与构造,笔者在第三单元的帮助下巩固了一些算法知识
在经过第三单元的训练后,笔者认为:JML规格设计是在实际工程开发场景中的一大有力武器。不难发现,JML规格语言提供了程序逻辑正确性的保证。JML的书写者只需着眼于程序整体的逻辑流程的正确性,而无需局限在每一个功能的具体实现上;而JML的阅读者则如本文开篇所说“戴着镣铐跳舞”,无须过多地考虑程序的整体逻辑是否正确合理,只需构建合理有效的数据结构对规格进行具体实现。可以看到,这里的书写者和阅读者,恰好对应着实际工程开发中的不同分工。不同于自然语言的多义、模糊,JML规格语言及其严谨,可以保证交流的双方不会出现理解上的差错,使得在实际的工程开发中能够更好地进行团队协作。
此外,JML既是设计的要求,也是测试的要求。从设计角度看,JML的require
语句对应着不同分支,ensure
语句对应着对应的效果,这给实现者提供了明确的方向;从测试的角度看,JML的require
语句对应着不同的数据约束,ensure
语句对应着应有的正确结果形式,这给测试者提供了全面的数据约束与正确性判据。
只是,JML的书写和阅读就目前来看总感觉有些“过度冗余”。从最简单的视角——行数来看,有时JML的规格行数甚至超过了实际实现的代码行数。期待之后会有更加方便的JML“编码”“译码”工具~