北航面向对象2022第三单元总结(JML)

北航面向对象2022第三单元总结(JML)

第三单元的作业以设计一个社交通讯网络为目标,帮助大家学习和理解JML的有关内容。值得一提的是,在本单元中,为了让大家更好地认识JML的有关内容和含义,图论的内容设计的相当多。并且,为了让大家有意识地控制自己的程序复杂度,也需要进行一些算法上的优化(并查集,迪杰斯特拉堆优化等等)。

JML——约束下的规格化设计

在第三单元刚开始的时候,我对JML基本是没有认识的。刚刚接触下来,也不过觉得JML似乎就是一个代码的简单表述和一些条件限制。

但是整个单元下来,并且从自己的错误中汲取了许多的惨痛教训,我深刻认识到,JML不是简单的简化或者约束,更应该是设计初的一个衡量标准、一个严格契约。它对实现者和使用者都提出了明确的要求和准则,而遵守这些准则就能够让整个开发过程不出差错。(当然这一切的前提是提出的JML本身足够完善)

另外,JML也用一种更加模式化的方法,让人们编程自动化的走向。毕竟非黑即白、统一标准、契约规则,这些都是电脑尤其擅长的领域。

三次作业中图模型的构建和维护策略

hw9: 尝试使用并查集

在第九次作业中,接口中定义的函数大多数都是相对简单的部分。其中比较复杂的只有query_circle相关的实现函数,它用来判明两个Person之间是否存在间接关系。在最初我没有多思考,通过每个Person管理的acquaintances直接使用了深度优先搜索的方法去遍历。

后来,经过讨论区和同学的提醒,我了解到进行有关操作实际上只需要用并查集的方法,维护根节点就可以满足本次作业的需求,并且也更容易扩展。并查集的最终效果是在查询结构上将森林中每一个图变成有且仅有一个根节点的树,这样,每一个图中所有点的前驱都是一样的,大大方便了查询判断与子图计数。

值得一提的是,并查集中的几个小方面。

其一:“查”的优化,在寻找根节点的过程中,把路径上的点不断向根节点靠拢,这里主要考虑路径减半和路径分裂的做法。

其二:“并”的优化。在Union的过程中,如果处理不当,我们合并得到的树甚至可能会退化到接近链表。为了避免,可以采用按秩(大小)合并等方法。

hw10:Kruskal与并查集

在第十次作业中,与图有关的函数集中在query_received_messages这个函数上。具体而言,涉及到类似最小生成树的一个算法。为了活用hw9中使用的并查集。我另外维护了“边” 的集合,方便在获得value时进行计算和替代。

由于并查集中根节点可以让我们明白哪些节点是互通的。从这个角度入手,我们可以优先筛选出与查询节点有关的“边”。这样,在运用Kruskal算法时,添加节点,选择边时,就可以忽略掉整个图中不连通的节点及其边,优化复杂度。

hw11:Dijkstra与堆优化

第十一次作业中,为了进行一个消息的间接发送,会进行最短路径的搜寻工作。为此比较容易想到要用Dijkstra算法进行搜寻。另外,使用堆优化,可以让每次循环添加最近节点变得轻松许多。而java提供了优先队列的容器,不用我们再额外新建堆优化的容器,因此实现起来也要轻松许多。

从JML角度来构建测试数据

在测试方面,我个人下了一些功夫去做覆盖,但是效果不佳,问题频出。后来和老师进行了一些讨论,存在一些需要改进的点,我就进行一个自我反思吧。

首先,随机生成数据仍然是一个很有效的方法。在第三单元的作业中,大家很容易发现,基本没有数据限制让某些指令必须在满足要求时才可能出现,因此只要满足格式要求的指令实际上都可以成功获取反馈。当然,需要限制id和value在一个合适的范围内,否则,可能难以命中正确实现的情况。毕竟随机id落点太广时,难以从正常角度实现指令,往往会以异常抛出的形式结束。这样,和小伙伴进行对拍,在测试开始的时候很容易确定是否存在宏观问题。

但是随机生成数据的一个弊端在于,自己难以定位bug。因此,单元测试还是十分有必要的。这里我们就可以从JML的角度去生成数据进行测试了,重点还是要做到覆盖(覆盖所有指令,覆盖所有指令顺序,覆盖所有异常和正常情况),而做到覆盖,可以从JML给出的多种前置条件出发(比如contains是否存在),组合不同的情况。

另外不可以忽视的是极端情况下自己程序的表现。在交流过程中也发现,有些同学在hw10中忽视了hw9中的群组最大人数1111的条件,导致问题。另外,互测中同学们也会以各种角度给出TLE的测试数据,这些实际上我们也可以预先考虑,测试程序的CPU运行时间。而JML给了我们一个切入点,可以判断哪些内容容易导致时间复杂度的上升。我们需要重点测试并且优化这些性能不足的部分,尽量做到天衣无缝。

性能问题与修复情况

在上一个部分我其实也基本介绍到了性能有关的测试问题。在这里我就详细展开谈一些细节吧,毕竟在作业过程中,我还是遇到过不少的性能问题。尤其集中在hw11。最后我的性能问题也是全部修复掉了。毕竟有问题摆在那里,总得想办法解决心里才踏实。(也是感谢各位帮我把问题找出来)

首先是阴险狡诈的O(n^2)。在hw10中,进行了关于query_group_value_sum 指令的测试。在JML中,这个指令简单地以两重循环地方式给出。我也没顾着有关内容,直接仿照着JML实现了。但实际证明,这是会T的(哭)。JML给出的只是一个规约,我们地实现不应该被规约所局限,要在满足规约限制的前提下,从一个实现者的角度去尽量改善有关内容的复杂度。另外,经过熟人的提醒,java给出的一些容器,不要看着查询和更改实现起来很友善,实际上背后暗藏玄机。一不小心可能也会导致O(n^2)的出现。(比如ArrayList的查询实际上最差有O(n),但是意识不到就会导致嵌套)

其次是堆优化的部分,也如上面所说,其实采用java封装好的容器就能比较简单的进行下去。毕竟这不是算法课,只要意识到要做出调整,基本不会有大问题。

另外还有一点是嵌套对于CPU时间的剧烈影响。在hw11的性能修复中,进行堆优化以后我发现自己还是不能满足要求。处理到最后,发现是我的一个拎出去单独封装的算法进行的嵌套递归冗余,导致CPU运行超时。经过优化后有了明显改善,恢复到了正常水平。

其它bug以及实现程序的体会

在本单元我还收获一系列其它的bug,总之就是很惨烈,在这里就不分单元详解了,宏观上谈一谈。

首先是很致命的格式化问题,比如输出时漏掉','让我痛不欲生。

其次是对JML实现上出现了偏差,比如情况考虑不完全,dev0的情况漏掉了;因为沉迷图的构造导致异常没抛出等等。

互测中,因为我也对自己的程序构造了许多的单元测试数据,因此基本上很容易从代码实现的角度发现其它人的bug。最后从互测上捡回了一些失去的分数。

其它还有一些JML实现的体会在此也谈一谈。例如图的构造,从JML的角度就只是告诉你是什么,但是实现你也要从自己的角度去设计;在hw10中增量开发时,回头看看hw9的代码实现很有必要,因为你也不知道新增的内容有没有涉及到过去内容的改动。在hw11中,需要考虑到继承后种种变化,这些在sm和am中都有新的改动,需要谨慎处理,既不能对原有的功能造成影响,也要确保自身内容的扩展符合新的JML规约。

Network扩展及规格撰写

对于扩展部分,首先由于这三种新的身份实际上是特殊的“Person”,因此我们可以定义三个新的接口去继承已有的接口Person,之后再用自己的My系列类来实现。此外,为了满足“广告”的要求,还需要定义新的AdvertisementMessage接口,继承已有的Message类,确保这种消息的沟通。而产品购买也需要一个ProductMessage接口,继承Message类。

具体来说,只有Advertiser可以发送广告的Message和购买的Message。广告信息类和产品购买类内部的id满足,一个ProductId来识别产品种类,sellId来识别产品具体的个体,也存有宣传的Advertiser和对应的Producer的人的Id。Customer内新增有自己的偏好对应产品的Id来表示。在发起购买时,Customer根据偏好从自己的信息里搜索对应的广告信息,然后从对应的Advertiser处发送购买信息给Producer,接着进行money上的增减,完成购买。

​具体各个接口的关键方法和属性如下:
​Advertiser:无新增接口方法,单独作为AdvertisementMessage和ProductMessage的发送者而存在,存有产品Id;

​Producer: 无新增接口方法,单独作为ProductMessage的接收者而存在,存有产品Id;

​Customer: 无新增接口方法,单独作为购买发起方而存在,内存有偏好产品Id,需要保证存在该产品;

​Product: 无新增接口方法,内部存储产品Id,产品价格。(代指一类产品)

​AdvertisementMessage: 内有属性advertiserId、producerId、productId(识别产品种类); (通过构造方法添加)

​ProductMessage: 内有属性advertiserId、producerId、productId(识别产品种类)、sellId(识别产品个体)、price(产品价格);

​MyNetwork:(新增许多方法)(内部维护两个新的容器,存储产品种类Id和产品销售额、存储已卖出的产品个体)
​addAdvertiser(int id, int productId): 添加销售员(首先他/她是个人)
​addProducer(int id, int productId): 添加生产人员(首先他/她是个人)
​storeProduct(int id, int money): 类似于storeEmojiId(int id)方法,为了识别对应的新产品而存在(注:需要维护一个ProductId的HashMap,目标与下面的销售额有关);
​containsProductId(int id): 类似于containsEmojiId(int id)方法,查询产品序列;
​queryProductSales(int productId): 查询某种产品销售额;
queryProductPath(int sellId , int sellId): 查询某个产品的销售路径;
​personContainsProductId(int personId, int productId): 查询某人是否含有对应广告信息
​buyIn(int personId, int productId): 某人购买某物

一些方法的修改:addMessage、sendMessage与sendIndirectMessage新增添加和发送广告信息和产品信息的方式。(buyIn会调用对应的方法来进行操作);

​一些崭新的异常:
​ProductIdNotFoundException: 不存在对应产品种类的异常;
​SellIdNotFoundException: 不存在对应产品个体的异常(在产品种类中);
​EqualProductIdException: 相同的产品Id;
​EqualSellIdException: 相同的产品个体Id;

​ 一些方法的JML书写:

    /*@ public normal_behavior
  @ requires !(\exists int i; 0 <= i && i < productList.length; productList[i].getProductId() == productId);
  @ assignable productList;
  @ ensures (\exists int i; 0 <= i && i < productList.length; productList[i].getProductId() == productId &&
  @           productList[i].getPrice() == price);
  @ ensures productList.length == \old(productList.length) + 1;
  @ ensures (\forall int i; 0 <= i && i < \old(productList.length);
  @          (\exists int j; 0 <= j && j < productList.length; productList[j].getProductId() == \old(productList[i].getProductId()) &&
  @          productList[j].getPrice() == \old(produceList[i].getPrice())));
  @ also
  @ public exceptional_behavior
  @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < productList.length;
  @                                     productList[i].getProductId() == productId);
  @*/
    public void storeProduct(int productId, int price) throws EqualProductIdException;

    /*@ public normal_behavior
  @ requires !(\exists int i; 0 <= i && i < productList.length; productList[i].getProductId() == productId);
  @ ensures \result == (\sum int i; 0 <= i && i < soldProductList.length;
  @           soldProductList[i].getProductId() == productId; soldProductList[i].getPrice());
  @ also
  @ public exceptional_behavior
  @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < productList.length;
  @                                     productList[i].getProductId() == productId);
  @*/
    public /*@ pure @*/ int queryProductSales(int productId) throws EqualProductIdException;
//@ ensures \result == (\exists int i; 0 <= i && i < productList.length; productList[i].getProductId() == productId);
    public /*@ pure @*/ boolean containsProductId(int productId);

学习体会

如果从三次作业的角度来说,比起第一二单元的内容,第三单元的内容一点也没有简单,或者说容量小。从认识JML本身,到彻底知道JML到底是用来做什么的,我经历了很多问题才醒悟过来。到现在,我才能回头惊呼:“哦!原来这就是JML!”

正如JML所揭示的,我们作为程序的实现者,要做到的是契约和规范。

从JML撰写的角度,客观并且全面的评价实现的功能,并且用一种约束化的语言来描述,是一个很困难的事情,也是一个很规范的问题。

从JML到实现代码,我们要依托JML提供的规则,小心不要误判规则,大胆超出表面进行自己的优化设计。这样才能够更好地完成给我们的任务。

posted @ 2022-05-31 19:08  2037hanzhe  阅读(90)  评论(0编辑  收藏  举报