BUAA_OO_第三单元总结
OO_第三单元总结
设计策略
本单元是关于JML的使用,JML可以提供具有严谨逻辑的规格,避免了自然语言描叙需求时可能存在的二义性。在完成作业的过程中我也体会到了JML规格的优势,因为阅读JML基本就是在阅读伪代码,省去了前两个单元理解题意后自行设计各个类和方法的过程。因此客观来说本单元的难度较为平和,但也正是因此,我对这个单元的重视程度不够,导致这一单元反而成为了我失分最多的一个单元。
在完成了三次作业后,我总结出来的设计策略应该是:
- 通读指导书和所有方法的规格,明确总体的任务是什么,如果是后两次作业,就要明白新增的任务是什么,再回顾上一次作业完成了什么方法,实现了什么功能。思考新增的任务对于之前已经实现的方法有没有新的要求。在思考结束后,再开始写代码。
- 选择合适的容器。规格中并没有指定所要使用的容器,都是描述了一个类似数组的结构。需要我们自己通过构思实现方法来决定。容器的选择直接决定了代码的复杂程度,以及之后效率优化的效果。
- 参照JML逐个实现方法,顺序就按照官方给出的接口中方法的顺序实现,课程组基本是按照方法之间的依赖关系来写接口的。较难的方法也一般在后面。对于实现起来较为简单的方法可以直接实现,对于一些逻辑相对复杂以及时间复杂度可能会很高的方法,就需要在保证规格的情况下自己设计。关于异常类的实现,建议是在完成方法的时候,遇到了哪个还没有实现的异常类,就顺便实现,因为这样可以比较充分的明白这个异常类是干什么的。
- 大致运行一下代码,检查出一些比较明显的错误,用对拍器等工具进行大量测试,检查时间复杂度会不会很高,继续修改自己的代码。对于在设计之初就已经进行了降复杂度的同学来说,这一步往往比较轻松,但是对于只用了最朴素的方法来完成的同学,可能就要在效率优化上下很多功夫。所以还是建议在阅读JML的时候就要对时间复杂度的问题加深思考。
- 最后,一定要对着JML仔细阅读一遍自己写的代码,看有没有笔误或者遗漏的地方。由于JML这一单元的特殊性,阅读代码我觉得是最有效的测试方法,一定要保证自己没有遗漏。
但非常可惜,我在完成作业的过程中,就非常的懈怠。拿到官方包之后,我往往是大概看一下就开始写,想快速对照JML写出大部分比较简单的函数,导致我写着写着就麻木了,不知道自己在写什么,不知道完成了什么样的功能,遇到比较难的函数还是得回过头来再看一遍,因为自己甚至都不记得刚刚写了什么函数,这是我从未有过的编程状态,可能是因为JML的特殊性,也可能是是我摸鱼心切。最后也没有进行过多的测试,也没有仔细阅读完成后的代码,导致我第三次作业因为一处笔误,强测只有50分。
最后我想说的是,虽然JML给出了很详细的函数实现过程,但是我们还是要把它当作指导书来看,而不是答案,编程时保持清醒的头脑,像前两个单元一样仔细思考,不要依附于JML而浑浑噩噩。
基于JML规格来设计测试的方法和策略
在第一次作业的时候我尝试过JUnit,但发现它并没有我想想中好用和有效,JUnit相当于是通过断言来判断这个函数在执行时是否和我们预期一致。这我就发现,对于简单的函数,用不到JUnit,毕竟十几行甚至更少的函数我还是比较相信的,而且这样的函数太多,写JUint往往不如直接阅读代码有效。而对于较为复杂的函数,我又不太会设计测试函数(菜是原罪
所以在这一单元,我主要用的测试方法还是通过对拍器和同学的代码进行对拍,来验证自己作业的正确性和时间效率。
容器选择和使用
JML中并没有规定具体要用什么样的容器管理数据,这就需要我们自己设计,做完后回顾,其实用什么容器都能完成作业,但合适的容器往往可以让代码的实现更加容易,对于效率优化也很有用。下面就讲一下我三次作业关于容器的选择。
第一次作业
第一次作业由于对规格的不熟悉,没有认识到容器的选择的重要性,所以容器我基本都使用的ArrayList甚至数组。
MyPerson
在实现MyPerson中我的acquaintance和value都是使用的ArrayList。
private ArrayList<Person> acquaintance = new ArrayList<>();
private ArrayList<Integer> value = new ArrayList<>();
ArrayList特点是想数组一样保存数据,可以记录下添加元素的顺序,比较适合管理,但是题目要求是每一个熟人都有其对应的社交值(亲密度?),要求二者是一一对应的。用两个容器管理容易出现差错,而且通过遍历查询比较浪费时间。在我看来可以使用
private HashMap<Person,Integer> person2value = new HashMap<>();
但由于三次作业中对Person的要求很有限,也没有删除熟人等操作,所以为了不引起更大的错误,我在三次作业中都采取了这样的容器。
MyNetwork
在实现MyNetwork中,people仍使用的ArrayList,在实现深度优先搜索时,visited用数组表示。
private ArrayList<Person> people = new ArrayList<>();
private boolean[] visited = new boolean[1500];
当然我这样写有些仅仅为了作业正确了,拓展性不够,最多只能支持visited数组大小个人。
反思一下,感觉可以使用HashMap和HashSet。
private HashMap<Integer,Person> id2person = new HashMap<>();
private HashSet<Person> visitedPeople = new HashSet<>();
第二次作业
吸取了第一次作业的教训,比较在意的选择了一下容器。
MyPerson
由于MyPerson中的messages规格中已经指定使用List,我就用了ArrayList,也确实比较符合使用场景,要求对信息按照先来后到排好队
private List<Message> messages = new ArrayList<>();
public List<Message> getReceivedMessages() {
ArrayList<Message> receivedMessages = new ArrayList<>();
for (int i = 0; i < 4 && i < messages.size(); i++) {
receivedMessages.add(messages.get(i));
}
return receivedMessages;
}
MyGroup
对于其中的people我使用了HashSet,因为确实Group中的人是一个集合,没有先后顺序之分,也不能有重复元素。使用HashSet实现addPerson(),delPerson(),等方法也比较方便。
private HashSet<Person> people = new HashSet<>();
MyNetwork
对于新增的两个成员groups和messages我都是使用HashMap,这样通过id找对应的对象非常方便,利于代码的书写和时间复杂度的把控
private HashMap<Integer, Group> groups = new HashMap();
private HashMap<Integer, Message> messages = new HashMap<>();
第三次作业
对于新增的要实现的emoji表情的热度维护,以及删除不常用的emoji。
private HashMap<Integer, Integer> emojiIdHeatList = new HashMap<>();
HashMap可以很好的实现相关功能,其中一个重点就是要学会如何用遍历器一遍遍历HashMap一边删除元素。下面列出部分代码
Iterator<Map.Entry<Integer, Integer>> it = emojiIdHeatList.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Integer> entry = it.next();
int heat = entry.getValue();
if (heat < limit) {
it.remove();
}
}
性能问题
在本次作业中,大部分函数根据JML的指导按部就班完成就可以,但少部分方法需要进行一定降低时间复杂度的方法,否则会在强测中超时,本单元涉及时间复杂度的地方主要有三个。
isCircle()
顾名思义,这个函数时见检查两个Person在社交网络Network中是否形成通路的,需要用到图的深度优先搜索。在第一次作业中,由于指令数不超过1000条,我使用了比较朴素的深度优先搜索,使用数组来实现visited,可能是我运气比较好,在第一次作业中没有出现超时,但我听说不少同学都因为仅仅使用了深度优先搜索而超时。所以我在第二次作业中改成了并查集搜索。
并查集搜索再实现时还需要注意一定的优化,否则一个小顶堆可能变成一条链,这也是对时间效率很不利的。
Group中平均量
在第二次作业添加Group时,我就觉得可能会在这里对于效率做文章,因为Group中有很多查询平均量的地方,如果一个Group中有很多人,而插平均量的指令很多的话,就会非常浪费时间,需要把每个人的age,value等先加起来,再平均,造成超时。所以我采用一些成员来帮我存放这个Group实例当前value,age,age的平方之和
private int valueSum;
private int ageSum;
private int age2Sum;
当每次有addPerson,和delPerson时,对这些值进行改变,这样可以很好的降低时间复杂度
public void addPerson(Person person) {
if (people.add(person)) {
addAtt(person);
}
}
private void addAtt(Person person) {
ageSum += person.getAge();
age2Sum += person.getAge() * person.getAge();
for (Person p : people) {
if (person.isLinked(p)) {
valueSum += 2 * person.queryValue(p);
}
}
}
但我也犯了一个很致命的错误,就是忘记了再第一次作业时实现了一个addRelation()的函数,如果添加关系的两个人刚好在同一个Group中,则valueSum就要增加,而我忽略了这一点,不过辛亏只错了一个点。这也提醒我们在完成这一单元的时候需要时常回顾上一次写了什么。
最短路径问题
在MyNetwork中有一个方法sendIndirectMessage()要求用最短加权路径发送信息,最短加权路径,那dijistra当仁不让,但是如果Network中人过多的话,对于当前最短距离的排序就很费时间,所以我采用了使用小顶堆实现的优先队列PriorityQueue来维护当前最短距离,自己新构建了一个类MyPoint来作为优先队列里的元素,存储的信息有人的id和这个人当前距离起点的最短距离,如果优先队列里没有,那就是当前距离还是无穷大。
关于dijistra算法就不过多赘述,下面附上我的代码
public int minPathDis(int beginId, int endId) {
Queue<MyPoint> points = new PriorityQueue<>();
MyPoint startPoint = new MyPoint(0, beginId);
points.add(startPoint);
Map<Integer, Integer> ans = new HashMap<>();
while (!points.isEmpty()) {
MyPoint tempP = points.remove();
int tempDis = tempP.getDis();
int tempId = tempP.getId();
if (ans.containsKey(tempId)) {
continue;
}
ans.put(tempId, tempDis);
MyPerson tempPerson = (MyPerson) getPerson(tempId);
for (Person ac : tempPerson.getAcquaintance()) {
if (!ans.containsKey(ac.getId())) {
points.add(new MyPoint(tempDis + tempPerson.queryValue(ac), ac.getId()));
}
}
}
return ans.get(endId);
}
但非常遗憾,我又一次把MyPoint中的compareTo函数实现错了,导致强测只有五十分。一个小小的笔误令人追悔莫及,但是也反映出我最近内心的浮躁,一定要调整心态,迎接烤漆的到来啊。
架构设计

关于架构设计,倒没有太多可以说的,因为所有的架构都是按找JML来实现的,基本就是每一个类实现一个官方包中的接口,其中MyEmojiMessage和MyRedEnvelopeMessage继承了MyMessage是这样的一种关系。
public class MyRedEnvelopeMessage extends MyMessage implements RedEnvelopeMessage
至于图的维护,MyNetwork中people中的每个元素都是图的一个节点,每个people的Acquaintance都是这个person的一个邻接点。此外,我觉得也没有必要进行更多图的维护,在查询图的邻接点时使用Person.getAcquaintance()方法,随即遍历get到的熟人列表即可。
感想
还是那句话,最平和的一个单元却失去了最多的分,导致前两个单元辛辛苦苦优化得到的性能分付之东流。还是有所遗憾的,但是这也为我敲响了警钟,最近一个月的学习状态确实不好,人有点麻木,浑浑噩噩的。还是希望自己能调整好状态,在最后一个单元中学到更多知识,获得一个不错的成绩。

浙公网安备 33010602011771号