OO_pre_4
第三次作业指导书
第一部分:训练目标
- 学习接口相关知识,并实践如何使用接口来建立行为层次结构。
- 学会使用 Java 类库提供的类进行排序。
- 掌握容器的克隆方法,理解浅克隆 (Shallow copy) 和 深克隆 (Deep copy)
- 初步了解 git 分支的用法
第二部分:预备知识
一、接口
前面我们提到了子类可以重写父类的方法,这使得子类的方法可以在父类的方法的基础上增加功能,或者实现一套和父类不同的新的功能。
倘若父类的抽象程度很高,以至于在父类中没有办法去编写一个实现具体功能的方法,我们可能会想是不是可以不写方法的具体实现语句,只定义方法签名呢?
比方说,正方形和圆形的面积计算很具体,假设为正方形和圆形建立了一个共同的抽象父类二维图形,此时如何去实现一个二维图形的面积呢?
比如下面的例子:
1 | class Square { |
很显然,我们无法为抽象的二维图形Shape类实现面积求解方法。此时,我们可以使用接口(Interface)来表示这个抽象的类,然后声明上述两个具体的类实现(implements)了这个接口:
1 | interface Shape { |
之后,你可以用接口类型来引用实现了该接口的任意类型的实例化对象,并调用接口所声明的方法。需要注意的是,你不能用接口类型来实例化一个对象:
1 | class Main { |
需要注意的是,接口提供了行为的抽象机制。在上面的例子中,Square和Circle的共性在于其行为操作,因而使用接口是合适的。对于其他一些情况,多个类之间可能即有共性的行为,也有共性的数据属性,此时使用类建立抽象层次更加合适。
在编程时,尽量使用高层次的引用(比如抽象类的引用和接口的引用),避免使用实际子类型的引用的方式,叫做面向抽象编程。下面我们会通过本 Task 让大家体会这一点。
二、浅克隆与深克隆
前面已经提到,在 Java 中,我们使用引用 (reference) 来操作一个对象。这表明,当我们在程序中写出形如 Bottle bottle
时,我们所声明的 bottle
变量只是一个引用,他可能会引用所有类型正确的实例。因此,如果我们需要对一个实例进行复制操作,就需要仔细考虑复制的是引用还是实例。请看下面的程序片段:
1 | class Main { |
我们可以发现,上面的程序只是对引用进行了克隆。上面的程序首先创建了一个 Square 实例,并使用 shape1 引用它。之后声明变量 shape2,并让 shape2 引用了 shape1 所引用的实例。我们只创建了一个实例,shape1 和 shape2 均为同一个实例的引用。因此,通过 shape1 引用对实例进行的修改,也会在使用 shape2 访问该示例时体现。
上面的这种只克隆引用的克隆过程,称为 浅克隆 (Shallow copy)。如果希望创造出一个“完整”的克隆,我们不仅要在编码时创建一个新的引用,还要创建一个新的实例:
1 | class Main { |
这种克隆引用和实例的克隆过程,称为 深克隆 (Deep copy)。
三、容器中的克隆
我们已经了解到,Java 使用引用来操作实例,这导致克隆时既可以克隆引用,也可以克隆实例,即深克隆和浅克隆。在之前的两次作业中,同学们已经学会了容器的基本使用方法。容器提供了管理多个对象的方法,字如其名,容器中”容纳了若干个对象”。在拷贝容器时,深拷贝和浅拷贝的区别将会加大。
现在请大家思考,一个对象是否可以位于多个容器中呢?答案是肯定的,这是因为 Java 中只能使用引用来操作实例,容器也不例外;每个容器维护的是若干个实例的 引用。因此,如果我们希望将一个容器进行拷贝,我们有两种方法:
- 使用浅拷贝,拷贝出的另一个容器管理的 引用 与原容器相同
- 使用深拷贝,先拷贝出该容器中管理的所有实例,再依次添加至新容器中
假设现在有一个名为 advs
的 ArrayList
容器,该容器管理了若干个 Adventurer
类型对象的引用。由于 Java 中所有类型都继承于 Object
类,该类拥有 clone
方法,因此我们使用 advs.clone()
对该容器进行克隆:
1 | advs.get(0).setName("Old Name"); |
可以发现,这是一个浅克隆,只克隆了容器中的引用,而没有克隆 Adventurer
对象。在 ArrayList.clone() 文档中已明确说明,该方法返回一个浅克隆的新容器:
public Object clone()
Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied.)
如果希望对容器每个对象本身都进行克隆,则需要遍历该容器,克隆其中的每个对象,并添加至新容器中。
在对容器进行克隆操作时,需要特别注意是否需要进行深克隆。
第三部分:基本要求
本次作业是本单元最后一次作业,仍需在上一次作业的基础上进行增量开发。
在本任务中,我们允许冒险者雇佣并使用另一个冒险者,且赋予冒险者价值的概念,把装备和冒险者都看作是价值体 commodity
。同时,我们还要对冒险者游戏增加版本管理功能,与 git 版本管理工具进行类比,可将冒险者游戏的状态视为需要管理的数据,每执行一条指令视为进行一次 commit,并实现简单的新建分支、检出分支功能。
第四部分:题目描述
- 增加 Commodity 接口,并使冒险者 Adventurer 类和装备 Equipment 类实现 Commodity 接口。接口中应定义冒险者和装备的共有方法,包括
useBy
方法等。 - 将原先的冒险者持有的 装备 的容器,更改为 价值体 的容器(即该容器可以容纳所有实现了 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
分支也会发生更改。
第五部分:输入/输出格式
第一行一个整数 m,表示操作的个数。
接下来的 m 行,每行一个形如 {type} {attribute}
的操作,{type}
和 {attribute}
间、若干个 {attribute}
间使用若干个空格分割,操作输入形式及其含义如下:
- 对 Task2 中的一些指令进行少量修改,重点地方已经加粗,并新增三条指令 10 ~ 12:
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}. |
一、数据范围与操作限制
变量约束
变量 | 类型 | 说明 |
---|---|---|
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
的分支。
二、测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值$x_{out}$ 和正确数值 $x_{std} $之间的绝对或相对误差小于等于 $10^{−5}$,则认为是相等的,即满足
$\frac{∣x_{std}−x_{out}∣)}{max(1,∣x_{std}∣)}≤10^{−5}$
三、输入输出示例
样例1
输入:
1 | 19 |
输出:
1 | 282 |
样例2
输入:
1 | 30 |
输出:
1 | 282 |
样例3
输入:
1 | 23 |
输出:
1 | 282 |
样例4
输入:
1 | 30 |
输入:
1 | 8269076524323616536 |
第六部分:提示
- 冒险者和装备都是价值体,都可以求价值、被使用以及字符串化等,故一个推荐的设计方法是建立价值体接口 ,接口中包含上述提到的三个方法,让冒险者
Adventurer
和装备Equipment
都实现这个接口,这样在顶层逻辑中就只能看到价值体这一种类型,可使用该类型的引用去调用不同子类型对象的这三种方法,这种处理称为归一化处理,会在正式课程中有专门的论述和训练。 - 本次作业将会涉及到自定义排序,请学习如何给对象编写
compareTo
方法并实现Comparable
接口,之后即可利用Collections.sort
方法来给容器内对象进行排序,考虑到有许多知识同学们还没有学过,本章结尾会给出一个例子,同学们可以照猫画虎地完成,compareTo方法仅需要在Equipment类中定义,Equipment类的子类如果不重写该方法的话,将会与父类行为保持一致。
与
Collections.sort
会调用compareTo
方法实现自定义排序,类似地,TreeSet
和TreeMap
容器也会通过调用对象的compareTo
方法,从而维护一个key对象有序的集合/映射。另外,
HashSet
和HashMap
这两种容器会通过调用对象的hashCode
方法和equals
方法来将任意对象作为key来使用。这个知识点非常重要,不过因为原理上与 compareTo 相似度较高便不在此处过多训练,请同学们务必弄懂其原理。Java中许多内置的类,比如
Integer
和BigInteger
等等都已经实现了compareTo
、hashCode
、equals
方法,所以你才可以直接把他们当作TreeMap
和HashMap
的key来使用。
1 | // Comparable接口的例子 |
- Title: OO_pre_4
- Author: Charles
- Created at : 2023-01-31 09:51:15
- Updated at : 2023-02-09 09:49:16
- Link: https://charles2530.github.io/2023/01/31/oo-pre-4/
- License: This work is licensed under CC BY-NC-SA 4.0.