BUAA OO 第二单元总结

22230604-艾俊辰 学生 2024-05-17 02:19:24

BUAA OO 第二单元总结

那年杏花微雨你说第三单元不考虑性能,转头压力测试和tle再续前缘……

记得认真阅读JML和随后的每一个更改通知啊!

一、程序结构

主要结构如下:

img

img

第二次作业加入了Tag类型,第三
次作业加入了
Message系列

1.代码思路

Runner负责分发函数,进入network进行进一步分发。

Person负责维护自己的Accquaintance、Tag及Value;Tag负责维护自己的People、ValueSum、AgeMean等;Group维护了整个关系网(即维护了图的集合);Message类等什么也没维护,起到一个存在的作用,NetWork负责了Meesage的add、send等功能。

2.分析结果

img

相较于前两单元而言,本次作业在JML的帮助下终于有较大进步。其中Group、MyPerson、MyNetWork三个类因为角色较为重要,可以说,MyNetWork和MyPerson是整个程序的存在基础,Tag、Message等都要依赖于Person,而Group维护了Person之间的关系。因此,三个类的耦合度高是自然而然的。

二、架构设计体验

偷着乐吧这个单元都把JML写好了
JML只规定了每个方法需要实现的内容,具体如何实现,仍然需要每个人自己去研究。换言之,当方法的规格确定,能够做文章的只剩下了算法。看了很多学长学籍的博客,经过和同学的探讨,也经历了两次作业压力测试的tle,把一点经验总结如下:

1. HashMap为什么是神

简言之,对比ArrayList、数组和LinkedList,HashMap以插入、删除、查找的复杂度都为1杀死了比赛。

首先是静态数组。本来对静态数组的恶意没有这么大,毕竟Java好用的容器太多了,要不是Test,静态数组甚至不会加入这个对比。但是人一旦尝试过HashMap想删就删、想插入就插入的爽之后,对于插入删除都很复杂的静态数组就会产生很大的意见。更有甚者,对于输入数据规格未知的互测强测,静态数组的规格取值不定,经常出现越界或者对应位置上元素为null的爆红。而一次性开很大的数组则有可能导致爆内存。

太久没写静态数组在Test卡了很久(对说你呢,提供返回值全部是静态数组的get函数),与C语言相比,Java的静态数组很好的一点在于,他可以通过

new int[emojis.size()]

方式来进行初始化,size可以是一个变量而非确定的数字。

静态数组太痛苦了愿此生不复相见。怪不得大家都不喜欢写C

其次是ArrayList。ArrayList好就好在它有序,对于依靠数组下标来定位或者要求特定顺序的方法很友好。但是,此处Person、Tag、Message等元素并没有强调顺序,特殊的id也让通过id查找变得更加方便,ArrayList在查找方面的优势并不明显,查找时间复杂度为n显而易见容易导致TLE。

最后是LinkedList。作为链表存储的List,查询时间复杂度依然为n,直接出局。

而我们亲爱的HashMap,插入、删除、查找时间复杂度都!为!1!而对于特定类型而言,id唯一代表其可以用作查找的key。伟大的HashMap门!

另一个在代码中出现的容器Hashset,虽然失去了HashMap的查找key,但很多Hash系列的自带函数还可以使用,这一部分留到后面详细分析。

总而言之言而总之,HashMap是神!!!!伟大无需多言。

2.无向图的最短路径和可达路径

省流版本:bfs和dfs,当年离散和DSP没学会的图开始攻击我,回旋镖正中眉心。

(1)可达路径

当两个结点之间的边被删除,如何判断这两个点是否仍然联通?在被删除的这条边之外,还有没有其他的可达路径?dfs和bfs都能够解决。但如何bfs或dfs才是关键所在。

这也是一大算法点,不好好实现就会喜提压力测试tle。

综合各方意见,我悟出来的经验是:尽量减少遍历。以下举例说明。

方法一:
dfs:
将图里所有结点的父节点设为自己,从根结点开始对所有节点进行遍历,遍历到的将父节点都设为根结点,如果有id2,则结束遍历。否则意味着需要分裂,再遍历全图找到所有父节点仍然为自己的点,将父节点设置为id2.
方法二:
(学习了一位大佬的方法改成了bfs,但是dfs同理)
将途中所有节点的flag设为false,从id1开始对每个结点的Acquaintance进行遍历,如果其中存在id2则结束。否则,将每一个遍历过的flag设为true,加入ArrayList,每次取出ArrayList[0],同时加入temp。最后将temp当中的所有结点变成一个新的图。

对一个有n个结点的图,方法一需要遍历三次,其中一遍是dfs,方法二只需要遍历两次+temp.size(),其中一遍是bfs,显然后者的遍历次数小于前者。其中差异就在于,如何实现dfs/bfs。方法一是最本真的实现方法,方法二是压栈出栈。两者性能相差数十倍,令人咋舌。

其实还是实现的问题,毕竟我的亲亲室友和隔壁大佬就没有tle

(2)最短路径

赤裸裸地要求每一个没学会图的小朋友上网络(CSDN)大学重修。

第二个算法点,第二次作业tle得愿赌服输。本地都要三十多秒能是什么好东西。

BFS搞定,但如何实现BFS就很有学问,原理通方法二。(哟又tle啦.jpg)

唯一需要注意的是,对于找最短路径,此处要分开考虑id1的熟人和他自己都输出0。

3.一些很NEWBEE的函数

部分回应了之前为什么HashMap是神,不仅仅在于复杂度低。

put,replace,remove为常见的插入、替换、删除,不再赘述。

免责声明:此处函数仅做介绍,详细使用条件等请上网查阅。

(1)hashmap.containsKey(Object key)

字面意思,是否存在对应的Key,返回值为boolean。

· hashmap.putIfAbsent(K key, V value)

putIfAbsent() 方法会先判断指定的键(key)是否存在,不存在则将键/值对插入到 HashMap 中。

注意,此处判断key是否存在是通过判断是否相等实现的,只能够判断基础类型和浅拷贝。此外,这里的判断方法也是遍历元素,对于时间复杂度的降低没有太大用处,更多是提升代码可读性。

(2)public String replaceAll(String regex, String replacement)

regex -- 匹配此字符串的正则表达式。

replacement -- 用来替换每个匹配项的字符串。

成功则返回替换的字符串,失败则返回原始字符串。

(3)hashmap.getOrDefault(Object key, V defaultValue)

key - 键

defaultValue - 当指定的key并不存在映射关系中,则返回的该默认值

唯一需要注意的一点在于,请确保defaultValue的值不会出现在某一个Value或者Key当中。血泪教训,最初defaultValue的返回值为-1,没考虑到Person的id也可能是-1,被hack得很惨。

(4)arraylist.removeIf(Predicate filter)

ArrayList的函数,HashMap也能用

filter - 过滤器,判断元素是否要删除

需要注意,此处的Value或者arraylist为null都会报空指针异常,注意判断前提条件。

(5)arraylist.retainAll(Collection c)

Hashset也能用(),前后属性一致即可。

保留 arraylist 中在指定集合中也存在的那些元素,也就是删除指定集合中不存在的那些元素,求而这的交集。

如果 arraylist 中删除了元素则返回 true。

如果 arraylist 类中存在的元素与指定 collection 的类中元素不兼容,则抛出 ClassCastException 异常。

如果 arraylist 包含 null 元素,并且指定 collection 不允许 null 元素,则抛出NullPointerException 。

与之类似:addAll:并集,removeAll:差集。

!!!注意 !!!
HashSet的retainAll会改变原对象的值,需要做好深浅拷贝的防范,防止不正确的更改(hw11de了两次哈哈

bug就像蟑螂,当你发现一个,暗处已经存在很多,下次改一个地方的时候记得看看其他类似思路的方法

(6)new HashSet<>(HashMap.values)

字面意思,可以通过这个方法将HashMap当中的values直接导出为成为一个Hashset,此处为浅拷贝。KeySet同理。

4.维护

避免tle的最大功臣,也是这次作业很大的收获。功夫在平时,只有平时作好维护,真正需要get的时候才能够有东西返回去。

其实维护本身不难,难的是考虑全面、合适的维护。

拿很痛苦的ValueSum举例,对于tag当中的ValueSum,什么时候需要改变?仅仅在addPerson和delPerson的时候吗?显然不是。

对于ValueSum,当两个在Tag中的Person关系Value改变,都应该进行增减。因此,应当在NetWork和Tag当中各自写一个newValuSum方法,对于addRelation和modifyRelation的情况进行增减。注意,如果是删边,记得减去的值是原来的权值,而非modifyRelation的参数Value。

那怎么做到考虑全面呢?在和室友大佬交流这个方法之前,我的方法是扔几组数据开始debug(……),说实话,很痛苦但也很爽。所以比较好的方法还是,写之前先想好,一气呵成,不要试图通过灵机一动、断断续续等方式来写维护,亲测无效。(除非你是大佬)维护只有两种写法,一气呵成或者debug,任君挑选。

5.static与final

意外的收获,其实是来源于Exception部分的报错。

之前对于xx变量应当为final的警告,我向来不以为意。这次不行,这次不设置为final跑不起来,遂学之。

来自网络,这就是final月static的定义和作用。

final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
static:用于定义类级别的变量和方法。这意味着它们属于类本身,而不是特定的实例。

对于异常类的计数器counter,必须明确:

1.它由所有异常的实例共享,由于每次抛出异常都是新建了一个对象,并且在new Exception方法当中实例化,因此是static,否则会出现每一个exception变量都有自己的counter,无法完成计数。

2.由于是共享,不能够有任何外部的方法去改变他(类内部自定义的方法除外),因此是final

6.JML规格与实现分离

JML规格比起指导每一步怎么实现,更像是告诉我这个方法需要完成什么

因为语言规范和清晰简洁的表意需要,JML所采用的方式一般较为原始和直接,例如数组这个自从写Java以来在也没用过的东西()而从性能角度出发,这显然是不明智的。无论主动还是被动,选择另外的容器与另外的实现方式几乎是必然的。

但是,无论怎么样的实现,最终都需要完成JML规格当中的要求。所以较为合理的思维方式应为:阅读JML-初步翻译为代码,原汁原味-进一步优化整合-撰写具体代码,在过程中进一步优化和整合。这个翻译应当是严格的。

三、bug分析

三次作业情况
hw9:强测tle了删边的情况,互测因为getorDefault返回值设的-1被打成了筛子

hw10:强测tle了qtvs,好狠毒的数据(抹泪

hw11:强测wa3个点都是因为没有认真阅读JML导致没排除除数为0的情况。其中一个点后来发现还要retainAll的问题。互测也因为没考虑tagSize为0的情况被hack了一次(

1.深浅拷贝

真好啊怎么又是你
先来看一个例子

private HashMap<Integer, HashSet<Tag>> tags;

HashSet<Tag> tags1 = tags.get(id1);
HashSet<Tag> tags2 = tags.get(id2);
tags1.retainAll(tags2);

结合上面对于retainAll的介绍,请问tags当中id1对应的HashSet会被改变吗?

答案是会的(要不然我怎wa了呢)

再来复习一遍

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)

对于上述代码,当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

所以,对于retainAll,其实会改变原来tags当中的HashSet,导致wa。

2.TLE

解决办法就是把双层遍历循环改成维护,把三次遍历改成双层遍历(一行字道尽多少心酸。累了,毁灭吧)

据说对于qtvs,虽然双层循环会tle,但将内层循环改成遍历每个Person的Accquaintance就能过,好吧!

3.请认真阅读TML和每一个更改通知(再次强调)

如题。

4.一些debug方法:

(1)断点:

找一些即使删除也不会对整体关系造成影响的(例如qts、qcs、qbs,此处以qts为例),定位了出问题语句后删除前面所有qts,而只在出问题语句前后加入qts观察变化。

缺点在于这种方法聚焦点比较小,多用于某个方法实现问题。如果这一句前后变化正确,则需要第二个方法。

(2)print:

确定出问题的id后在相关函数内部写print函数观察变化,一般格式为:变化之前、变化中、变化后。

tips.可以将无关输出(例如Ok、抛出异常等)先注释掉,避免干扰;也可在输出前后加'='进行突出显示。

缺点在于如果该ID的tag/person等变化很多,输出量很大,会得不偿失。一般适用于变化不多、涉及语句较小的id。

四、JUnit测试

一半时间都花在这个*测试上了哈哈哈哈哈

很痛苦,测试找出bug远比修改bug来得困难得多,尤其bug代码还不公开。Junit测试全靠大家互帮互助(大佬带带)

1.数据生成

经过与室友的交流,Junit的生成大概有两种思路:随机法与完全图法。其中随机法的思路等都来自我伟大的吴老师,链接在这里:https://bbs.csdn.net/topics/618693628

随机法,顾名思义,节点、关系、权值、Message等都随机,语句生成也随机,即下一条是send还是addMessage都随机,通过大数据量来完成覆盖。

完全图法是我跟另一位大佬学的。顾名思义,是建立一个固定结点数的图,通过一点点加边再一点点删边来完成情况的全覆盖,即从全部为点到完全图再到全部为点。对于hw11,我采用了在完全图的情况下对每两个人都进行所有类型Message的add与send。

完全图法的好处是其节点数和边数都是已知、可控的,对于for循环等可以自信地写确定的上限(到底在好什么),不会出现因为随机生成数据导致wa的赌博。

坏处就在于,完全图法太可控、太对称、太均匀,以至于一些情况无法被覆盖到。

以hw11为例,如果仅仅是对每两个person加入一个mojiMessage,每次delete(0),那么实际上无人伤亡,真正删除表情的部分就没能够测试到。而如果改成delete,list会变成null,后续数据就相当于没有作用了。

对此,我的解决方法是delete之前先对每两个人add和send5个emojiMessage,再选取两个人加入第六个,最后delete(5),通过了case1。

hw10同理。

2.判断方法

hw9和hw10需要测试的函数当中都有pure部分,因此我选择了影子network的方法。对于network1和2,前面所有操作都相同,最后一个选择调用需要被测试的方法,一个选择实现该方法(就是把本地的该方法实现搬过来),之后按照JML的限制条件逐条判断

hw11不再有pure限制,因此设置一个network即可。为了同步,我在test当中设置了Messages和heat两个Hashmap模拟add、send、delete等操作,用于对拍。

三次test对拍的时候记得首先判断size是否相同,如果不同的话就可以拉出去埋了,没必要继续,此外,如果遇到需要开很大的数组进行get方法实现的,记得判断是否为null和数据大小(例如Persons总不能够超过你设置的人数吧)

不得不说提供的方法返回数组真的很难用,但有时候,可以借助数组进行简化:如果正常实现方法,eg.network1.getA[i] == network2.getA[i],不必再寻找是否存在j使得network1.getA[i] == network.getA[j]。所谓对拍嘛,就是这样。

3,黑箱测试与白箱测试

一言以蔽之,黑箱测试是关注测试代码的功能,关注输出的正确性,不考虑内部结构或实现细节,就像Junit检验JUnit测试代码一样,最后输出只有Pass和Fail。

白箱测试是需要了解代码的内部实现,可确定测试用例是否覆盖了所有的代码路径和逻辑分支,就像Junit本身检验本地代码,任何爆红都需要找出来到底是谁的bug。

JUnit和我从上学期到这学期一直相爱相杀,痛并快乐着。一方面,JUnit确实增加了很多工作量,debug量增加了,还要担心JUnit的测试能否通过;另一方面,JUnit确实测出了不少本地代码的bug,也让我进一步认识到了好的数据应该是什么样的:清晰简洁、覆盖完全、逻辑简单,不是简单的数据堆砌,其中道行值得研究。(也就是所谓的数据强不强,能不能测出bug)

五、心得体会

JML再见,虽然我还是不太会写你,但是没关系,会读就够了

  • 都在说这一单元简单,但真正实现起来,不管是hw9对于JML的疑惑不解还是后续对于图的再学习,难度都不小。而压力测试的存在又让人不得不在基本实现翻译JML之外进行一定的优化,综合来看其实不算轻松。
  • 令人开心的是,不管主动被动,在各路大佬的帮助下,还是学到了不少东西,种种原因想着翻译完的就好,总结下来还是优化了不少
  • 最好的是,这一单元心态好多了。因为架构实现难度不大,debug最大的困难是数据量太大,重构的时间成本并不会太高,所以抱着“大不了重构”的态度,反而比较顺利。最搞心态的反而是JUnit,因为不知道什么时候能过、能不能过,不过还好,方法选取较为合理,需要手搓特殊关系、边界关系的数据比较少,还算顺利。
  • 不喜欢看通知,也不老老实实看JML,痛失30分,肉痛,下次不许这样了!!!(尖叫
...全文
42 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧