oo pre前三次作业总结
题面
1.基本要求
- 本次作业是本单元最后一次作业,仍需在上一次作业的基础上进行增量开发。
- 在本任务中,我们允许冒险者雇佣并使用另一个冒险者,且赋予冒险者价值的概念,把装备和冒险者都看作是价值体 commodity。同时,我们还要对冒险者游戏增加版本管理功能,与 git 版本管理工具进行类比,可将冒险者游戏的状态视为需要管理的数据,每执行一条指令视为进行一次 commit,并实现简单的新建分支、检出分支功能。
2.题目描述
- 增加 Commodity 接口,并使冒险者 Adventurer 类和装备 Equipment 类实现 Commodity 接口。接口中应定义冒险者和装备的共有方法,包括 usedBy 方法等。
- 将原先的冒险者持有的 装备 的容器,更改为 价值体 的容器(即该容器可以容纳所有实现了 Commodity 接口的类)。
- 定义冒险者的价值为其拥有的所有价值体的价值之和,即冒险者的价值是其装备的价值及其雇佣的冒险者的价值的和。
- 增加冒险者之间的雇佣关系:冒险者 A 雇佣冒险者 B,可以认为是把冒险者 B 看成一种价值体。此时冒险者 A 拥有价值体冒险者 B,之后冒险者 A 便可以像使用其他装备一样使用冒险者 B。
- 定义冒险者 A 使用冒险者 B,其效果为冒险者 A 按照价值从大到小、价值相同则按价值体 id 从大到小的顺序 依次使用冒险者 B 的价值体,价值体的价值指的是所有价值体在本次使用前的价值。我们规定:如果当前使用到了冒险者 B 雇佣的冒险者 C,则冒险者 C 要按照如上顺序使用其拥有的价值体,这些价值体将作用于最开始使用的冒险者,在此处即为冒险者 A。
- 新增版本管理功能:我们仿照 git 中的分支机制进行版本管理。将每一条执行的指令视为一次 commit,初始状态下默认分支名称为1,需要支持“创建分支并检出该分支”功能,以及“检出”功能。与 git 相同,每次 commit 都将移动当前 HEAD 指针所指向的分支指针,也就是说,假设当前处于 br 分支,执行了若干条指令(相当于在 br 分支上进行了若干条 commit)后,br 分支也会发生更改。
3.输入、输出格式
第一行一个整数 m,表示操作的个数。
接下来的 m行,每行一个形如 {type} {attribute} 的操作,{type} 和 {attribute} 间、若干个 {attribute} 间使用若干个空格分割,操作输入形式及其含义如下:
type attribute 指令含义 输出 1 {adv_id} {name} 加入一个 ID 为 {adv_id}、名字为 {name} 的冒险者,且未持有任何装备 无 2 {adv_id} {equipment_type} {vars}(equipment_type和vars的含义见下表) 给予某个人某件装备,装备类型由 {equipment_type} 定义,属性由 {vars} 定义,所有的瓶子初始默认装满 无 3 {adv_id} {id} 删除 ID 为 {adv_id} 的冒险者的 ID 为 {id} 的价值体,如果被删除的价值体是冒险者,则解除雇佣关系,后续无法使用该被被解除了雇佣关系的冒险者,如果删除的价值体是装备,则丢弃该装备,后续该冒险者无法使用该装备 无 4 {adv_id} 查询 ID 为 {adv_id} 的冒险者所持有价值体的价格之和,如果价值体是装备,则价值就是 price,如果价值体是冒险者,则其价值计算按照本 Task 最开始定义的规则 一个整数,表示某人所有价值体的价值总和 5 {adv_id} 查询 ID 为 {adv_id} 的冒险者所持有价值体价格的最大值,如果价值体是装备,则价值就是 price,如果价值体是冒险者,则其价值计算按照本 Task 最开始定义的规则 一个整数,表示该冒险者所有价值体价格的最大值 6 {adv_id} 查询 ID 为 {adv_id} 的冒险者所持有的价值体总数,如果价值体是装备,则对总数的贡献是 1,如果价值体是冒险者,则只要考虑被雇佣冒险者本身这一个价值体即可,不需要考虑被雇佣冒险者所拥有的其他价值体,即对总数的贡献也是 1 一个整数,表示某人所有价值体的数量之和 7 {adv_id} {commodity_id} 打印 ID 为 {commodity_id} 的价值体的全部属性 该价值体的全部属性,格式见下文“属性打印方式” 8 {adv_id} ID 为 adv_id 的冒险者按照价值由大到小的顺序使用其全部价值体,若价值相同则按照价值体的 id 由大到小的顺序使用。(价值体价值为所有价值体本次使用前的价值)若当前使用的是价值体是装备,这次使用的效果同Task2中的规定,若当前使用的价值体是冒险者,这次使用的效果已在第四部分中规定。 每个价值体在使用时就会产生输出,除此之外无额外输出 9 {adv_id} 打印 ID 为 {adv_id} 的冒险者的当前状态。 一个字符串表示冒险者的状态:The adventurer’s id is {adv_id}, name is {name}, health is {health}, exp is {exp}, money is {money}. 10 {adv_id1} {adv_id2} ID 为adv_id1的冒险者雇佣 ID 为adv_id2的冒险者 无 11 {branch_name} 在当前状态新建分支,分支名称为 branch_name。与 git 类比,相当于在当前状态创建一个名为 branch_name 的分支,并检出该分支:git branch ${branch_name} && git checkout ${branch_name} 或 git checkout -b ${branch_name} 无 12 {branch_name} 切换到版本名称为 branch_name 的分支,之后的更改也将应用于该分支,详见“题目描述”部分。与 git 类比,相当于检出名为 branch_name 的分支:git checkout ${branch_name} 无 vars 和 equipment_type 如下:
装备类型 equipment_type vars Bottle 1 id name price capacity HealingPotion 2 id name price capacity efficiency ExpBottle 3 id name price capacity expRatio Sword 4 id name price sharpness RareSword 5 id name price sharpness extraExpBonus EpicSword 6 id name price sharpness evolveRatio 属性打印方式表格:
价值体类型 属性打印方式 Bottle The bottle’s id is {id}, name is {name}, capacity is {capacity}, filled is {filled}. HealingPotion The healingPotion’s id is {id}, name is {name}, capacity is {capacity}, filled is {filled}, efficiency is {efficiency}. ExpBottle The expBottle’s id is {id}, name is {name}, capacity is {capacity}, filled is {filled}, expRatio is {expRatio}. Sword The sword’s id is {id}, name is {name}, sharpness is {sharpness}。 RareSword The rareSword’s id is {id}, name is {name}, sharpness is {sharpness}, extraExpBonus is {extraExpBonus}. EpicSword The epicSword’s id is {id}, name is {name}, sharpness is {sharpness}, evolveRatio is {evolveRatio}. Adventurer(新增) The adventurer’s id is {id}, name is {name}, health is {health}, exp is {exp}, money is {money}.
4.数据范围和操作限制
变量约束:
变量 类型 说明 id, adv_id, adv_id1, adv_id2, commodity_id 整数 取值范围:0 - 2147483647 name 字符串 保证不会出现空白字符 装备的 price 长整数 在 long 精度范围内,且保证不小于0 capacity, efficiency, expRatio, sharpness, extraExpBonus, evolveRatio, health, exp, money 浮点数 在 double 精度范围内 branch_name 字符串 只包含数字和字母 操作约束:
操作数满足 1≤m≤2000。
保证所有价值体的 ID 两两不同。
操作2-9:冒险者 ID 一定存在。
操作 3,7:冒险者一定持有该 ID 的价值体。
操作 4,6:冒险者不持有任何价值体,则输出 0。
操作 5:冒险者一定持有至少一个价值体。
操作 10:雇佣和被雇佣的冒险者均已存在,且不是同一个冒险者。
指令 11:新建的 branch_name 不与已有的 branch_name 重名。
指令 12:检出的 branch_name 之前一定被新建过。
冒险者的雇佣关系不会存在循环雇佣的情况,每个冒险者最多仅能被一个其他冒险者雇佣一次。
初始状态下位于 branch_name 为 1 的分支。
题解
由于是增量开发,和前两次作业类似的位置即略写
- 1.类(接口)创建和初始化:
类的创建和继承关系如下图:
- 先考虑价值体相关类的创建:首先是Adventurer类和Equipment类,这两类都属于价值体Commodity,因此都需要接入价值体接口。Equipment可以细分为六种,通过继承也可以显现其从属关系,即HealingPotion和ExpBottle属于Bottle,而RareSword和EpicSword属于Sword(即”is a”),同时在真实情况中每一个冒险者都拥有自己的一堆价值体,所以需要在adventurer类中创建一个价值体容器来收纳属于该冒险者的价值体
- 后考虑分支相关:team类体现的是当前的状态,每一个状态其实取决于所有冒险者的状态,因此team类中拥有一个adventurers容器,收纳所有的冒险者,组成了当前的状态。branch是管理所有分支的类,其中拥有teams的容器,相当于收纳了所有的状态,想要切换状态只需要让main函数中的状态变量指向容器中合适的team即可。
Adventurer初始化:
1 |
|
Equipment初始化:
1 |
|
Bottle初始化:
1 |
|
此处注意super的使用,由于Bottle继承Equipment,则同时也可以继承equipment类中的构造函数,此处的super(id,name,price)就相当于equipment的构造函数,较为方便并能体现继承性,其他装备类的初始化同理。
Team和Branch类的初始化:
1 |
|
- 2.操作1,2,3
操作1就是将数据读取后利用构造函数创建新adventurer并放入team中的adventurer容器:
1 |
|
操作2就是把数据读取后利用构造函数创建新equipment并放入对应冒险者的价值体容器:
1 |
|
(此处是利用输入的装备类型数用switch case体系进行判断类型并针对类型选择构造函数)
操作3就是删除某冒险者价值体中对应id的价值体,如果是人的话即使删除也会依然保存在Team中的adventurers容器中(从操作10可知此处就是从team中浅拷贝来的),而如果是装备则直接删除就好,所以无论如何都可以直接使用容器的remove。
1 |
|
注意:由于此处对不同价值体都要使用相同的getId()
函数,所以需要在commodity接口中先定义此函数,然后在adventurer和equipment中分别复写即可(记得写@override
)
- 3.操作4,5,6
操作4是查询某一冒险者的价值之和,则只需要使用一个获取价格的函数即可,但是获取价格是针对所有价值体都有效的,所以应该把这个函数写在commodity接口中,然后再在冒险者和装备中复写,而装备也应该在继续细分后在子类中复写。而针对冒险者,则需要再向下递归,获取这个被雇佣的冒险者的价格之和。(此处展示冒险者价值获取)
1 |
|
注意:由于本题的价格之和有可能超过Long的限制,所以需要用java自带的Biginteger类来保证数据不会超出范围。
操作5和操作4比较类似,本质还是对选定的冒险者的价值体进行获取价值,然后选取最大价值的价值体输出即可。
1 |
|
为了方便之后的排序使用功能,可以编写compareTo函数和Comparable接口来进行比较排序。为了对整个价值体容器进行排序,需要在价值提接口extends Comparable<Commodity>
,然后在adventurer和equipment类中分别复写compareTo函数(我感觉类似于C语言中qsort后面写的排序方法)本题要求高价优先,同价高id优先,则compareTo函数写法如下(两个子类写法相同),上面的操作5其实是借用了这个函数进行比较。
1 |
|
操作6就是获取价值体数量,直接getsize即可,略过。
- 4.操作7,9(打印相关)
操作9是打印一个冒险者的状态,则直接将冒险者的状态get出来然后进行打印即可。
1 |
|
操作7就是打印某一个人的某一个价值体,因此只需要把printSituation函数写在commodity中即可(因为意识到操作7,9对于冒险者的打印方式相同),然后在两个子类中分别复写,对于装备类在对更多的子类继续复写即可,略过。(由于Sword和Bottle的打印方式完全不同,所以装备类中的打印状态函数其实就是一个空函数)
- 5.操作8
操作8涉及到两个问题:排序以及使用。 - 首先考虑排序问题,由于之前已经进行了Comparable接口的继承和compareTo函数的写入,因此只需要使用Comparable接口的sort函数进行排序即可。
- 对于使用问题,需要注意到我们需要对使用者做出更改和相关打印,因此所有使用函数都需要传入使用者(一个adventurer类),同时使用也可以使用人或物,所以这个使用函数应该先在价值体接口中写再分别复写。
- 因此总体流程就是在使用函数中传入一个使用者,利用排序函数将使用者的价值体排序,再分别使用。
1 |
|
(冒险者类中的使用函数)
同理,由于bottle和sword的差别,在装备类中的使用函数就是空壳,也需要分别复写(下面是bottle类中的使用函数,还需要在expbottle和healingpotion类中分别复写)
1 |
|
- 6.操作10
操作10就是让一个冒险者雇佣另一个冒险者,即找到另一个冒险者的id然后将其加入雇佣者的价值体中即可,但是要注意此处加入价值体容器需要是浅拷贝,这样雇佣者价值体容器中的被雇佣者和team中的冒险者容器中的被雇佣者用同一片内存,可以一起进行状态改变。
1 |
|
- 7.操作11,12(分支相关)
这两个操作(尤其是操作11)才是本次作业的精髓!(这些操作都在Branch中完成)
分支11就是状态的拷贝和跳转: - 在一开始的时候就需要new一个team表示初始的状态,并且默认其名字为“1”:` public Team() {this.branchName = “1”;} //默认命名
- 然后对于不同状态,就需要对team容器中所有的冒险者进行拷贝,只要两个team容器中的状态(除了名字)完全一致,此时主程序的team又被新的team赋值,此时就完成了分支的创建和跳转。
- 对于深拷贝,由于无论是冒险者还是物品都需要拷贝,因此也是在价值体接口中先写一个函数在分别复写。对于一个冒险者,深拷贝就是先new一个新冒险者,把所有冒险者本身的属性提取出来再赋给新冒险者(get-set),最后再进行价值体的拷贝,价值体中的物品就按照物品方式拷贝,人就继续使用这个拷贝函数。
(冒险者类中的拷贝函数)
1 |
|
对于物品的深拷贝方式,就是创建新物品,然后先get再set,最后输出新物品即可,但是本次作业我没有分别在子类中继续复写深拷贝函数而是利用了向下转型,先判断该物品是什么类型(instanceof)再进行拷贝,这种方式比较笨重,故省略。(但是注意要先判断小类别,例如经验瓶和治疗瓶,而后判断大类别,例如瓶子,否则经验瓶等会被优先当作瓶子处理)
但是要注意,所有东西都深拷贝后放入新team并不代表状态完全还原!因为没有完全还原雇佣关系,举例说原来A雇佣了B,但是在深拷贝后,由于是分别对A,B进行拷贝,因此A的价值体中的B和Team中自带的冒险者容器中的B并没有指向同一个位置!这也就代表新状态中这两个B没有联系。因此我们需要为这两个B建立一个浅拷贝关系来让他们共用同一个内存空间。
所以本题我的思路就是在拷贝完成之后进行一个检查,一旦出现这种状况就进行处理。但是这里出现了一个我找了很久都没有找出来的bug!!!!!!
我的原代码如下:
1 |
|
我的思路就是利用递归,对冒险者容器中的每一个冒险者的价值体检索,如果和这个冒险者容器中的其他冒险者id相同,证明这个冒险者的价值体中有另一个冒险者,此时就对这个被雇佣的冒险者也做这样的检查,直到这个冒险者的所有价值体都不和team中的冒险者容器中的id重复,证明所有价值体都是装备,此时就把这个冒险者删除,并且把team中的这个冒险者拷贝到此处。以此类推。
但是后来在和qsh的讨论中发现,这个递归好像没有必要!只需要检测到第一层即可(就是直接把check函数那一行注释掉)因为team中的冒险者容器中的冒险者含有所有的雇佣关系,所以直接对第一层的冒险者进行删除和浅拷贝就可以,反而如果递归到底,例如第三层雇佣关系的人先被处理,但在第二层雇佣关系的人被处理时,之前第三层关系的处理会被删除,因此递归毫无意义!
但是的但是,其实是否递归应该并不影响结果(但是比对了结果其实影响了……这到现在都是一个未解之谜),那么Bug出现在哪里呢?经过我俩的思考和讨论,最后被我给发现了。以下是修改之后的代码:
1 |
|
其实问题就出在删除和浅拷贝上了……假设一个冒险者的价值体容器中连续两个元素都是冒险者,那么我首先检测到第一个冒险者应该处理,这时我利用了removeCommodity函数,但是其实这个函数就是在adventurer类中对价值体容器的in1个元素的普通remove(arraylist内置方法),此时后面的那个冒险者就会往前进一位(remove就是这样,被删除元素后面的元素集体前进一位来避免空白),而循环的in1变量又加一了,导致后面那个冒险者没有被处理。
解决方法就是每次检测到需要处理了,就把in1也减1,这样可以保证下一个元素也被检测,同时加入的时候add也是放在价值体容器最后,因此可以不用再次检测,就把检测范围减一(此处用gg变量表示检测范围),这样就对啦!
操作12就是在Branch类的team容器中找符合分支名的分支并跳转,较为简单,略过。
结语
至此第三次作业的总结也就完成啦!写完还是非常有成就感,尤其是把自己debug的思路完整的写出来真的很开心。同时这也是我第一次使用markdown写这种类似博客的总结,就更有成就感啦!希望自己也能继续加油!