「BUAA OO Unit 3 HW12」第三单元总结

「BUAA OO Unit 3 HW12」第三单元总结

Part 0 前言

值得一提的是,笔者在做作业中探索了适合自己的策略,和大家分享:

  • 首先通览guidebook,了解大致需求以及用到的算法
  • 实现异常类
  • 实现一般类中新增的内容,可以用Tricks一节中的插件进行比较快速得知更新内容

Part 1 第九次作业

1.1 作业要求

根据给定JML编写符合要求的在限制复杂度内的Java代码。

具体内容为实现GroupNetworkPerson三个类,模拟一个社交网络中的群体、个体及其关系;实现六个抽象异常类,要求具有计数功能。

本次作业的主要难点有:JML的入门;自定义异常类与抛出;连通块维护。

1.2 JML入门

这一部分主要根据课程组提供的JML level0指导书学习,我将相关笔记整理在博客「BUAA OO Unit 3」 JML笔记中。

1.3 自定义异常类与抛出

本次作业带我实现了对自定义异常类从0到1的突破,并让我得以窥得异常一隅。

自定义异常类也是一个类,抛出的自定义异常也是一个对象(符合Java中一切皆对象的思想),我们的具体操作就是在产生异常(这里的异常指自定义异常,即,条件判断中符合自定义异常条件)的时候new一个异常对象,并使用throw语句将其抛出,非常形象。

try-catch

对于本次作业需要自己完成的异常部分到此为止,但是通过Runner类,我们可以较为完整地了解异常机制。

我们抛出异常后并没有到此为止,异常会被沿着栈的方向被抛给上层调用方法;在这个过程中,如果异常被某个调用方法捕获并处理,则不再上抛;如果所有方法都选择将此异常抛出,那么最终这个异常会被JVM(Java Virtual Machine)捕获,并终止程序的运行。

对于上述内容的实现,在Runner类里可以看得分明。以下以addPerson为例:

RunneraddPerson方法

	private void addPerson()
            throws InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        int id = Integer.parseInt(cmds[1]);
        String name = cmds[2];
        int age = Integer.parseInt(cmds[3]);
        try {
            network.addPerson((Person) this.personConstructor.newInstance(
                    id, name, age));
        } catch (EqualPersonIdException e) {
            e.print();
            return;
        }
        System.out.println(String.format("Ok"));
    }

其中调用的我们的MyNetwork类的addPerson方法实现如下:

    @Override
    public void addPerson(Person person) throws EqualPersonIdException {
        if (contains(person.getId())) {
            throw new MyEqualPersonIdException(person.getId());
        } else {
            /* when add a new person, it has no relationship with anyone
            * so, just make a new block for it*/
            people.put(person.getId(), person);
            MyBlock myBlock = new MyBlock(person.getId());
            myBlock.addPerson(person);
            blocks.add(myBlock);
        }
    }

我们注意到,Runner中的try块包裹了我们的addPerson调用,并在catch块中调用了异常对象的print方法作为对这一异常的处理。我们还可以注意到,Runner依旧有四个继续抛出的异常,这说明异常的处理自由灵活,可以根据需求在任一阶段处理任意异常并将剩余异常继续向上抛出。

通过上面的例子,我们较为完整地了解并实现了异常的设计、定义、实现与处理过程。

1.4 连通块维护

JML仅描述了设计的需求,并没有限制实现。因此,对于如queryBlockSum等方法,直接翻译JML是正确的,但是并不满足我们对于性能的要求。在这里,我主要实现了连通块类Block

Block中,我设计了储存块内Person的容器,实现了mergeBlock对两个连通块进行合并。

在第十次作业中我们将看到,这个设计对于维护和迭代开发大有裨益。

1.5 bug分析

自己bug

本次作业在中测、强测和互测中没有出现bug。

别人bug

本次作业房间中没有测出bug。

1.6 Tricks in HW9

  • getOrDefault

使用:hashMap.getOrDefault(Object key, V defaultValue)

这是一个HashMap的方法,获取指定key对应的value,如果找不到key,就返回设置的默认值defaultValue

  • 并查集

Block是一个连通块,用并查集维护高效可靠。

  • 自定义异常

自定义异常类需要继承Exception类或其子类,举例如下:

public class MyEqualPersonIdException extends EqualPersonIdException {
    private int id;
    private static int totalCnt = 0;
    private static HashMap<Integer, Integer> idTimeHashMap = new HashMap<>();

    public MyEqualPersonIdException(int id) {
        this.id = id;
        totalCnt++;
        int time = idTimeHashMap.getOrDefault(id, 0) + 1;
        idTimeHashMap.put(id, time);
    }

    @Override
    public void print() {
        System.out.println("epi-" + totalCnt + ", " + this.id + "-" + idTimeHashMap.get(id));
    }
}

当我们需要某一个方法能够抛出这个异常类的异常的时候,举例如下:

public void addPerson(Person person) throws EqualPersonIdException {
    /*int len = people.toArray().length;
    for (int i = 0;i < len;i++) {
    if (people.get(i).equals(person)) {
            throw new
        }
    }*/
    if (contains(person.getId())) {
        throw new MyEqualPersonIdException(person.getId());
    } else {
        /* when add a new person, it has no relationship with anyone
        * so, just make a new block for it*/
        people.put(person.getId(), person);
        MyBlock myBlock = new MyBlock(person.getId());
        myBlock.addPerson(person);
        blocks.add(myBlock);
    }
}

“异常”也是对象,我们可以通过自定义异常类来关注我们需要的信息。在这里,我们记录了计数器的信息。


Part 2 第十次作业

2.1 作业要求

在第九次作业的基础上,增加首发私聊与群发消息、查询最小关系(查询最小生成树)等功能,并为新增功能实现新的异常类。

算法上,新增涉及最小生成树求解,这是本次作业的主要难点。

最小生成树

在第九次作业中,我已经维护了一个连通块类Block,在本次作业中,我们只需要让Block增加边集属性并维护,就可以得到网络中的所有连通分支,这是我们算法的基础。

这里我采用了Kruskal算法。具体来说,得到连通分支后,初始化每个点为单独的分支,逐个加边,每次加边调用find,find采用朴素的路径压缩,每次将被访问的点及其所有父亲的祖宗节点更新(如果需要)为祖宗节点;一条边的两端得到两个祖宗节点,更新一个祖宗节点的祖宗节点为另一个祖宗节点,这时候其实并没有将被合并的树的深度降低为2(应该是3),但是被合并的树的叶节点再次被调用find路径压缩的时候就会更新为2。

2.3 bug分析

自己bug

本次作业在中测、强测和互测中没有出现bug。

但是,在自己搭建的评测姬测试时,出现了若干bug,以下分别介绍。

  • sendMessage中忘记在网络中移除已经发送的消息。这一bug产生的原因是阅读JML时读完前文忘记后文,没有及时用自己的语言总结需求导致漏掉了限制。这一bug并没有在中测中测出。
  • queryLeastConnection的最小生成树算法实现有误,没有实现路径压缩,导致在部分情况性能极差。这一bug没有在中测中测出。

别人bug

互测中共🔪中四次,两位同学各两次,均为qgvs实现复杂度过高,为 $ O(n^2) $ ,在极端数据下会超时。

通过上述bug分析我们可以看到,尽管本单元架构无需设计,算法也较为基础,但是对于细心和复杂度计算仍有要求,特别是在中测只完成了基础功能测试,比较容易通过的情况下,更需要大家使用包括JUnit和随机数据轰炸等方式保证自己的正确性和复杂度。

自本次作业开始建造和修改评测姬,进行大规模对拍,为自己和hxd拍出了不少bug。在其中,我们对数据范围特别是指令条数等限制较少,基本远超作业要求,以期更快发现问题。


Part 3 第十一次作业

3.1 作业要求

在第十次作业的基础上,增加红包首发(单发与群发),首发emoji并对emoji的热门程度排序,首发间接消息(堆优化的迪杰斯特拉)

等功能,并为新增功能实现新的异常类。

算法上,新增涉及堆优化的迪杰斯特拉算法,这是本次作业的主要难点。

堆优化的迪杰斯特拉

sendIndirectMessage方法中,需要查询两点间最短路。朴素地,我们可以用无优化的迪杰斯特拉直接完成,复杂度为 $ O(n^2) $ ,如果实现了堆优化的迪杰斯特拉,可以将复杂度降低至 $ O(nlog(n)) $ 。

具体来说,朴素的迪杰斯特拉算法中的行为如下:

  • 从未求出最短路径的点中取出距离起点最小路径的点,认为其为最短距离。值得注意的是,未连通我们认为距离为无穷大。
  • 用第一步中找到的这个点的距离更新未求出最短路径的点。当然,只有在可以更新(能连通而且通过该中转点距离更小)的时候才更新。

我们通过粗略的分析可以得知上述过程复杂度为 $ O(n^2) $ 。堆优化的要点在于,我们第一步中寻找最近点的操作,可以用优先队列(堆)进行优化,将这一步的复杂度从 $ O(n) $ 降低为 $ O(log(n)) $。如此,便降低复杂度到 $ O(nlog(n)) $ 。

3.2 bug分析

自己bug

本次作业在中测、强测和互测中没有出现bug。

在使用评测姬进行随机数据对拍时,发现以下问题:

  1. EmojiIdNotFoundException使用错误,应当传入的参数是emojiId而不是messageid
  2. clearNotices错误地边遍历边删除
  3. MyRedEnvelope的构造器中少写了一个赋值money
  4. 在堆优化的迪杰斯特拉算法中,没有在抵达终点就退出,而是遍历了整张图

别人bug

互测中共🔪中1次,这位同学的sendMessage逻辑出错,没有和JML吻合。

3.3 Tricks in HW11

PriorityQueue

Java提供的PriorityQueue容器可以自动维护堆,我们只需要重写比较器即可。

以下以private PriorityQueue<Node> queue;为例介绍可能用到的操作:

poll()

取出堆顶元素。

e.g. Node x = queue.poll()

add()

插入元素,此时优先队列会帮我们维护好堆。

e.g.queue.add(new Node(fromId, dis.get(fromId)));

compareTo()

为优先队列中的元素重写排序方法,以便于容器的维护。

e.g.

public int compareTo(Node o) {
    return Integer.compare(this.getDis(), o.getDis());
}

Part 4 Tricks

在本单元作业中,有一些可能难登大雅之堂,但是非常实用的小技巧,在这里总结分享。

IDEA compare with clipboard

快速对比两次作业之间的区别和迭代。

本单元的一个特点是,指定的任务并没有在guidebook中明确介绍,而是以JML的形式给出,当两次作业迭代时,通过这个插件可以快速比对出更改的内容。

VSCode实现JML高亮

这是本单元最有用的小技巧之一可惜我在最后一次作业写完之后才发现

背景

IDEA中缺少相关JML语法高亮的插件或配置,看官方包时的效果如下:

image

解决办法

在VSCode中“语言模式”选择“Java + JML”,可以对注释部分进行高亮,效果如下(和上图为同一个函数):

image


Part 5 单元架构设计

本单元的接口和主体框架已经由课程组给出,且三次作业为迭代开发关系没有重构,下面展示第11次作业整体结构:

image

图的维护

在本单元中,MyBlock是表现比较好的有一个类,该类的对象维护了连通块中的点集(Person)和边集,在第9词作业引入后,为后续两次作业的迭代开发带来了很大便利。


Part 6 Network拓展

题目要求

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)


以下选择发送广告、购买商品和设置偏好撰写JML规格:

发送广告

    /*@ public normal_behavior
      @ requires containsAdvertisement(id);
      @ assignable advertisements;
      @ ensures !containsAdvertisement(id) && advertisements.length == \old(advertisements.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(advertisements.length) && \old(advertisements[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < advertisements.length; advertisements[j].equals(\old(advertisements[i]))));
      @ ensures (\forall int i; 0 <= i && i < people.length; people[i].isFavorable(id) ==> 
      @             (\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] == id) &&
      @              people[i].advertisements.length == \old(people[i].advertisements.length) + 1);
      @ ensures (\forall int i; 0 <= i && i < people.length; !people[i].isFavorable(id) ==> 
      @             !(\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] == id)) && 
      @              people[i].advertisements.length == \old(people[i].advertisements.length));
      @ ensures (\forall int i; 0 <= i && i < people.length; 
      @             (\forall int j; 0 <= j < \old(people[i].advertisements.length)
      @                 (\exists int k; 0 <= k < people[i].advertisements.length;  
      @                     \old(people[i].advertisements[j]) == people[i].advertisements[k])));
      @*/
    public void sendAdvertisement(int id);

购买商品

    /*@ public normal_behavior
      @ requires contains(personId);
      @ requires containsProduct(productId);
      @ requires containsSaler(salerId);
      @ ensures getPerson(personId).money = \old(getPerson(personId).money) - getProduct(productId).getValue;
      @ ensures getSaler(salerId).getProduct(productId).getLeftNum() = \old(getSaler(salerId).getProduct(productId).getleftNum()) - 1;
      @*/
    public /*@ pure @*/void purchaseProduct(int personId, int productId, int salerId);

设置偏好

    /*@ public normal_behavior
      @ requires contains(personId);
      @ requires containsProduct(productId);
      @ ensures getPerson(personId).isFavorable(productId) == true;
      @*/
    public /*@ pure @*/void saleProduct(int personId, int productId);

上述JML中还依赖于一些其他的基础功能,基本可以望文生义,因此不再额外声明


Part 7 回顾与展望

第三单元以社交网络模型为载体,通过短平快的三次作业的迭代开发让我初步了解JML的基础语法与实战应用,并尝试利用JUnit等工具进行评测。同时,通过几个简单的算法应用让我体会到规格的定义是严谨的,而实现是灵活的,同样的需求用更小的代价实现是我们应当考虑的内容。

平心而论,本单元的挑战性和需要克服的困难较前两单元有所下降,但是尽管如此,想要做到尽善尽美也非易事,特别是琐碎繁杂的需求使得测试和debug变得困难,尽管多次改造开发了评测姬,但是没能达到100%数据全覆盖,甚至连是否达到80%都不好说,对于极端数据和压力数据更是需要靠手动构造,这都为强测与互测带来了风险。

一如既往地,本单元作业中,老师、助教和同学们给予了我非常大的帮助,没有大家的支持,我不可能顺利走过这个单元,再一次向大家表示由衷的感谢。

写这篇回顾总结的时候,我也正在Unit4中挣扎,而且并不轻松,希望可以轻装上阵,准备充分地面对OO的最后一个单元。

posted @ 2022-05-31 16:20  被水淹没的一条鱼  阅读(234)  评论(0编辑  收藏  举报