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

对于新增的两个成员groupsmessages我都是使用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来实现的,基本就是每一个类实现一个官方包中的接口,其中MyEmojiMessageMyRedEnvelopeMessage继承了MyMessage是这样的一种关系。

public class MyRedEnvelopeMessage extends MyMessage implements RedEnvelopeMessage

至于图的维护,MyNetwork中people中的每个元素都是图的一个节点,每个people的Acquaintance都是这个person的一个邻接点。此外,我觉得也没有必要进行更多图的维护,在查询图的邻接点时使用Person.getAcquaintance()方法,随即遍历get到的熟人列表即可。

感想

还是那句话,最平和的一个单元却失去了最多的分,导致前两个单元辛辛苦苦优化得到的性能分付之东流。还是有所遗憾的,但是这也为我敲响了警钟,最近一个月的学习状态确实不好,人有点麻木,浑浑噩噩的。还是希望自己能调整好状态,在最后一个单元中学到更多知识,获得一个不错的成绩。

posted @ 2021-05-29 18:01  mjw0803  阅读(47)  评论(0)    收藏  举报