「BUAA OO Pre」 Pre 2总结回顾概览
Part 0 前言
写作背景
笔者在完成寒假预习作业Pre2系列任务时遇到了一些挑战并有一些收获和心得,在这里记录和大家分享。
定位
基于本篇博客,您可以了解笔者在实现Pre2尤其是迭代开发中的心路历程及踩过的坑。
为了读者的阅读体验,本篇博客将按照task顺序
展开。
为了博客的完整性和顺利展开,对于课程组guidebook中的题目要求部分将做引用,若有侵权请联系即刻删除相关内容。
您可以在这里期望获得
- 笔者从做题者的视角的收获
- 笔者踩过的坑
您在这里无法期望获得
- 部分知识的深层原理
对读者前置知识的期望
- 完成《程序设计基础》/《C语言程序设计》和《数据结构》课程
- 有一般学生C语言的水平
- 了解Java语言的基本语法,此部分知识可以通过各种在线资源获得
Part 1 Pre 2 task 1
题目
描述
先介绍 pre2 练习的背景故事。
想象你是一个冒险者,现在正在一个新的星球上进行探险,这个过程中你需要通过努力收集各种物品来不断增强自身能力值,在第一个 task 中你需要对第一个基本物品 Bottle 进行建模。
经过之前的预习和上面的练习现在到了实战的时候了:构造一个 Bottle 类,来表示冒险者需要用到的瓶子类,要求 Bottle 类包含属性:ID,名字,价格,容量,和表达瓶子是否装满的标志量。相应的要求完成的程序可以查询瓶子的 ID,名字,价格,容量以及是否装满。
一开始给出一个瓶子。之后,有 7 种操作:(序号即为操作数)
- 查询名字
- 查询价格
- 查询容量
- 查询是否装满
- 更改价格
- 设置是否装满
- 输出瓶子描述字符串
输入/输出格式
第一行给出 整数 id
(取值范围为0-2147483647
),字符串 name
,长整数 price
、浮点数 capacity
分别表示瓶子ID,名字,价格,容量,以空格分隔。
第二行一个数 m
接下来 m{type} {attribute}
的形式来描述,具体内容如下:
type | attribute | 意义 |
---|---|---|
1/2/3/4 | 无 | 打印名字/价格/容量/是否装满(每个瓶子在创建之后默认最开始是装满的) |
5 | {price} |
更改价格为 {price} |
6 | {filled} |
更改瓶子装满的状态为 {filled} |
7 | 无 | 以The bottle's id is {id}, name is {name}, capacity is {capacity}, filled is {filled}. 的形式打印状态。 |
建议在 Bottle
类中定义 toString
方法,返回描述字符串,在主类的 main
方法中调用 Bottle
对象的 toString
方法来打印。
数据范围与操作限制
变量约束
变量 | 类型 | 说明 |
---|---|---|
id | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符 |
price | 长整数 | 在 long 精度范围内 |
capacity | 浮点数 | 在 double 精度范围内 |
操作约束
- 操作数满足 1≤m≤2000。
测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值 xout 和正确数值 xstd 之间的绝对或相对误差小于等于 10−5,则认为是相等的,即满足
输入样例
12345667 water 20 100
8
1
2
3
4
5 30
6 true
7
2
输出样例
water
20
100.0
true
The bottle's id is 12345667, name is water, capacity is 100.0, filled is true.
30
提示
下面简单介绍 pre2 系列任务的迭代形式:
从 Task1 到 Task3 ,逐步引导同学们实现一系列基础的类,并且熟悉类、属性和方法的使用,引导大家向面向对象的思维方式转变。
Task4 涉及方法的重写和复用,并引入异常处理机制,希望同学们可以感性地体会到层次化设计的好处,了解并简单应用异常处理(异常处理在之后也常会用到)。
Task5 涉及接口,需要同学们在之前 Task 的基础上完成更加复杂的操作。如果此时仍然使用原来的编码习惯,会在这个 Task 中遇到巨大困难,而严格按照我们的提示去做的同学会体会到好处。
强烈建议按照从 Task1~Task5 的顺序完成 pre2 的练习,思考如何进行增量迭代和持续重构,而不要在实现每一个 Task 时都完成一份新的代码。
值得注意的点
- 读者应当先按照pre1中工具链相关介绍完整正确配置好所有相关配置,特别是IDEA的checkstyle及new file settings等,磨刀不误砍柴工。
- 建议每一个类(class)都新建一个
.java
文件,一方面保证高内聚低耦合,另一方面使得结构层次清晰。 - 站在Pre 2 task 5的视角来看,从这里开始就应当逐渐学习并掌握静态方法和静态变量的书写方式,以在未来更复杂的输入情况中依然可以遵守checkstyle要求的类行数不超过60的要求。
- 时刻记得checkstyle,以保证代码风格符合规范。
- 使用
package
管理当前作业的文件。 commit
到课程仓库的只需要所有.java
文件,不需要其他文件。
Part 2 Pre 2 task 2
题目
基本要求
- 建立冒险者类,且符合封装的要求
- 使用适当的容器管理多个冒险者实例
描述
在这个问题中,你需要管理多个冒险者。初始时,你没有需要管理的冒险者。接下来会有 m 个操作:
- 加入一个需要管理的冒险者
- 给某个冒险者增加一个瓶子
- 删除某个冒险者的一个瓶子
- 查询某个冒险者所持有瓶子的价格之和
- 查询某个冒险者所持有瓶子价格的最大值
你需要对操作 4、5 进行回答。
输入/输出格式
第一行一个整数 m,表示操作的个数。
接下来的 m 行,每行一个形如 {type} {attribute}
的操作,操作输入形式及其含义如下:
type | attribute | 意义 | 输出文本 |
---|---|---|---|
1 | {adv_id} {name} |
加入一个 ID 为 {adv_id} 、名字为 {name} 的冒险者,且未持有任何瓶子 |
无 |
2 | {adv_id} {bot_id} {name} {price} {capacity} |
给 ID 为 {adv_id} 的冒险者增加一个瓶子,瓶子的 ID、名字、价格、容量分别为 {bot_id} 、{name} 、{price} 、{capacity} ,且默认为已装满 |
无 |
3 | {adv_id} {bot_id} |
将 ID 为 {adv_id} 的冒险者的 id 为 {bot_id} 的瓶子删除 |
无 |
4 | {adv_id} |
查询 ID 为 {adv_id} 的冒险者所持有瓶子的价格之和 |
一个整数,表示瓶子价格之和 |
5 | {adv_id} |
查询 ID 为 {adv_id} 的冒险者所持有瓶子价格的最大值 |
一个整数,表示瓶子价格的最大值 |
数据范围与操作限制
变量约束
变量 | 类型 | 说明 |
---|---|---|
id (adv_id, bot_id) | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符 |
price | 长整数 | 在 long 精度范围内 |
capacity | 浮点数 | 在 double 精度范围内 |
操作约束
- 操作数满足 1≤m≤2000。
- 保证所有冒险者与瓶子的 ID 两两不同。
- 操作 1:不会加入与已有冒险者和瓶子 ID 相同 ID 的新冒险者。
- 操作 2:冒险者 ID 一定存在,且新瓶子的 ID 与当前所有冒险者和瓶子的 ID 均不相同。
- 操作 3:冒险者 ID 一定存在,且冒险者一定持有该 ID 的瓶子。
- 操作 4:冒险者 ID 一定存在,若冒险者不持有任何瓶子,则输出 0。
- 操作 5:冒险者 ID 一定存在,且冒险者一定持有至少一个瓶子。
测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值 xout 和正确数值 xstd 之间的绝对或相对误差小于等于 10−5,则认为是相等的,即满足
输入样例
7
1 1 Person1
2 1 2 bottle1 10 5
2 1 3 bottle2 15 12
4 1
5 1
3 1 2
4 1
输出样例
25
15
15
提示
建立一个对象的集合,实现向集合中增加对象和访问集合中对象的操作,学习容器的使用和选择。
熟悉对容器的操作,题目中限制了所有对象(冒险者、瓶子)的 ID 不会相同,思考一下,哪种容器会更加适合本次任务?或者说哪些容器呢?
在本次作业中我们有求和操作,尽管我们将输入数据限制在 long 的范围内,但是在求和时可能会超出精度范围。请你查阅 Java 相关资料,来看看在 Java 中是如何解决超过普通数据类型数据范围的精度问题的。
Java 中有些特别的类用于处理大数运算,如 BigInteger
,BigDecimal
。
值得注意的点
-
了解
BigInteger
相关知识并应用,这将在下面的扩展知识进行部分介绍。 -
了解Java中的一种遍历方式如下:
for (Bottle bottle : bottles) { max = Math.max(bottle.getPrice(),max); }
该遍历方式可以便捷遍历如
ArrayList
等容器内的所有对象,优于C语言中按索引遍历的方式,尤其是我们不关心容器内对象的索引的时候。对于各种容器的使用,可以首先参考Java ArrayList - 菜鸟教程,了解ArrayList
基本用法后迁移知识到其他容器如HashMap
等。
扩展知识
BigInteger
主要的构造方法
BigInteger(String val)
使用举例:BigInteger eg = new BigInteger("100");
常用方法
-
加减乘除:
加法:
BigInteger add(BigInteger val)
减法:
BigInteger subtract(BigInteger val)
乘法:
BigInteger multiply(BigInteger val)
除法:
BigInteger divide(BigInteger val)
-
获得两个
BigInteger
中的最大/最小值最大值:
BigInteger max(BigInteger val)
最小值:
BigInteger min(Biginteger val)
-
获得(长)整型(
long
)的BigInteger
对象static BigInteger valueOf(long value)
Part 3 Pre 2 task 3
题目
基本要求
- 建立类 Equipment,所有的装备均继承自这个类(该类因而可称为基类, base class),请将所有装备都具有的属性定义在这个类里。
- 建立 Bottle 类与 Sword 类,应当满足
- 符合某种继承关系
- 具备信息查询方法
- 实现各项装备的查询和增删指令
具体实现细节将在“题目描述”中展开
题目描述
现在我们将在上一个任务的基础上对 Bottle 进行细分,并添加新的“武器类”——Sword
药水类型 | 属性 | 属性类型 |
---|---|---|
HealingPotion | 包括 Bottle 的全部属性,新增加属性 efficiency,代表药水的治疗效果 | Bottle 原有属性不变,efficiency 为浮点数类型 |
ExpBottle | 包括 Bottle 的全部属性,新增加属性 expRatio,代表水瓶对于经验值的增强效果 | Bottle 原有属性不变,expRatio为浮点数类型 |
武器类型 | 属性 | 属性类型 |
---|---|---|
Sword | sharpness,表示武器的锋利程度 | sharpness 为浮点数类型 |
RareSword | 包括 Sword 的全部属性,新增加属性 extraExpBonus,代表使用武器的附加效果 | Sword 原有属性不变,extraExpBonus 为浮点数类型 |
EpicSword | 包括 Sword 的全部属性,新增加属性 evolveRatio,代表使用武器的附加效果 | Sword 原有属性不变,evolveRatio 为浮点数类型 |
将有以下操作:
- 加入一个冒险者
- 给某个冒险者添加某件装备(装备包括药水和武器)
- 删除某个冒险者拥有的某个装备
- 查询某个冒险者所拥有装备的价格之和
- 查询某个冒险者所拥有装备的价格最大值
- 查询某个冒险者拥有的装备总数
- 打印一个装备的全部属性,属性的输出顺序与输入创建该装备时给定的各参数顺序一致,具体格式详见下方
属性打印方式
输入输出
第一行一个整数 m,表示操作的个数。
接下来的 m 行,每行一个形如 {type} {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} {equipment_id} |
删除 ID 为 {adv_id} 的冒险者的 ID 为 {equipment_id} 的装备 |
无 |
4 | {adv_id} |
查询 ID 为 {adv_id} 的冒险者所持有装备的价格之和 |
一个整数,表示该冒险者所有装备的价格总和 |
5 | {adv_id} |
查询 ID 为 {adv_id} 的冒险者所持有装备价格的最大值 |
一个整数,表示该冒险者所有装备价格的最大值 |
6 | {adv_id} |
查询 ID 为 {adv_id} 的冒险者的装备总数 |
一个整数,表示该冒险者所有装备的数量之和 |
7 | {adv_id} {equipment_id} |
打印 ID 为 {equipment_id} 的装备的全部属性 |
该装备的全部属性,格式见下文“属性打印方式” |
装备类型 | 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}. |
数据范围与操作限制
变量约束
变量 | 类型 | 说明 |
---|---|---|
id | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符 |
price | 长整数 | 在 long 精度范围内 |
capacity, efficiency, expRatio, sharpness, extraExpBonus, evolveRatio | 浮点数 | 在 double 精度范围内 |
操作约束
- 操作数满足 1≤m≤2000。
- 保证所有冒险者与装备的 ID 两两不同。
- 操作 1:不会加入与已有冒险者和装备 ID 相同 ID 的新冒险者。
- 操作 2:冒险者 ID 一定存在,且新装备的 ID 与当前所有冒险者和装备的 ID 均不相同。
- 操作 3:冒险者 ID 一定存在,且冒险者一定持有该 ID 的装备。
- 操作 4:冒险者 ID 一定存在,若冒险者不持有任何装备,则输出 0。
- 操作 5:冒险者 ID 一定存在,且冒险者一定持有至少一个装备。
- 操作 6:冒险者 ID 一定存在,若冒险者不持有任何装备,则输出 0。
- 操作 7:冒险者 ID 一定存在,且冒险者一定持有该 ID 的装备。
测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值 xout 和正确数值 xstd 之间的绝对或相对误差小于等于 10−5,则认为是相等的,即满足
输入样例
9
1 1 Person1
1 2 Person2
2 1 1 3 bottle1 10 5
2 1 6 4 sword1 20 7 0.6
2 2 3 5 bottle2 15 3 8
6 1
7 2 5
3 1 3
5 1
输出样例
2
The expBottle's id is 5, name is bottle2, capacity is 3.0, filled is true, expRatio is 8.0.
20
补充材料
-
请思考,本次作业中的求和操作等是否会出现超出数据限制的情况。
提示:Java 中有特别的类:
BigInteger
和BigDecimal
。 -
设计模式是软件开发人员经过相当长的实践总结出来的最佳设计方案,在面向对象设计与构造课程中,你将逐步了解和掌握几种基本的设计模式,包括工厂模式、单例模式、生产者-消费者模式等。
现在,希望大家可以了解工厂模式,这是在继承和接口实现中常用的设计模式。
大家可以参考链接中的介绍,也可以自行查阅资料。这将帮助你更轻松的完成日后的作业 😃
值得注意的点
- IDEA去掉空白快捷键:
Ctrl + Shift + J
;格式化代码:Ctrl + Alt + L
。 - 子类继承父类:
extends
关键字。 ArrayList
和HashMap
主要使用特点和区别,这将在下面的扩展知识具体介绍。- 继承中的向上转型、向下转型与方法重写,这将在下面的扩展知识具体介绍。
扩展知识
ArrayList
和HashMap
基础用法对比
ArrayList |
HashMap |
|
---|---|---|
构造方法 | ArrayList<E> objectName =new ArrayList<>(); 其中E 是泛型数据类悉尼港,用于设置objectName数据类型,只能为引用数据类型,若是基本数据类型需要使用其包装类,如int ->Integer |
HashMap<Integer, String> Sites = new HashMap<Integer, String>(); 其中键值对类型可以相同或不同 |
添加元素 | boolean add(E e) |
V put(K key, V value) |
删除元素 | boolean remove(Object o) boolean removeIf(Predicate<? super E? filter) |
V remove(Object key) |
获取容器容量 | int size() |
int size() |
获取特定元素 | E get(int index) |
V get(Object key) |
对于更细致的用法和特性,可以参考Java ArrayList和Java HashMap。
由以上对比可以看到,HashMap
具有可以根据key
值随机访问和删除的功能,而ArrayList
在这方面的支持是按照索引访问,即类似数组下标。根据本task要求可以知道,冒险者拥有的物品的id
是我们关心的,我们并不关心其在列表中的索引下标,因此,使用HashMap
是一种更优越的选择。
对于遍历操作,ArrayList
支持直接使用类似如下格式,可以直接遍历每个对象:
for (Bottle bottle : bottles) {
/* body */
}
HashMap
则支持对key
值遍历,在此过程中可以通过get
方法访问每一个key
对应的对象,也做到了遍历:
for (Integer i : valueEntityHashMap.keySet()) {
sum = sum.add(valueEntityHashMap.get(i).getPrice());
}
向上转型、向下转型和方法重写
向上转型:可以使用父类型去引用通过子类型创建的对象,此时程序设计者只关心父类型层次可以看到的方法,因此只能调用父类型所声明的方法。
如果该引用转型前和转型后所属的类型都声明了同样原型的方法,那么访问哪个方法体就会涉及方法查找的问题,一句话说明即:从对象创建时的类开始,沿类层次向上查找。
上述的两个特征为我们实现此task及后来的task提供了一种方法和思路:对于冒险者对象而言,其要管理其名下的诸多Equipment
(到task5还会涉及到ValueEntity
等),但是创建对象时又很纷繁,如Bottle
类、Sword
类等,如果对每个具体的子类都创建ArrayList
或者HashMap
进行管理,显然极大增加了复杂度,降低了可维护性和可拓展性。
考虑到上述方案的缺陷,同时考虑到Java向上转型及方法查找的特点,我们可以建立一个ArrayList
或者HashMap
容器,其保存对象(或Value
)为Equipment
,这样既可以方便统一管理,又可以正确调用对象的方法。值得一提的是,该方案可行离不开题目中要求输出的均为可在父类Equipment
中声明的方法及每个对象的id
都不同而可以作为key
这两个题目条件。
向下转型:如果对父类型引用直接调用子类型对象特有的方法,则在编译阶段即被认为是错误,因为我们的设计想要的就是在父类型引用阶段就使用当前视角。如果我们有一个指向实际存在对象的有效引用,同时知道指向的对象的创建类型是其引用类型的子类,而且我们想调用子类特有的方法(父类不存在),这时Java提供了关键词instanceof
以判断对象引用所指向的对象的创建类型是否是某个特定的类,一般写为obj instanceof A
,其中obj
为对象引用,A
为一个类型(类或接口),该式值为boolean
,若obj
创建类型为A
,则为true
,反之则为false
。在该表达式值为true
时,可以使用向下转型来用一个A
类型的对象来引用obj
:A ao = (A)obj
。值得注意的是,在上述过程中obj
的创建类型一直没有变化,进行转型的中只是对象引用类型。
方法重写:在向上转型部分中我们提到了,父类型引用可以指向父类型的子类对象,同时这个引用可以调用父类型声明的共有的方法,并且具体访问的哪个方法体会涉及到方法查找,这就引出了方法重写的问题。
方法重写,即在子类中对父类声明过的方法重新书写,在IDEA中进行该操作时,会自动在重写方法上方补充一行@override
,其作用有两个:一个是本身为伪代码,表示该方法为重写;另一个是编译器可以验证@Override
下面的方法名称是否是父类所有的,如果没有就会报错。
Part 4 Pre 2 task 4
题目
描述
- 冒险者除了具有 id, 姓名 name, 拥有的装备 equipment 等状态外,还增加了另外的一些属性:生命值 (
health
, 浮点数,默认值 100.0)、经验值(exp
, 浮点数,默认 0.0)、金钱数(money
, 浮点数,默认 0.0)。 - 每一种装备都有一个使用方法,定义如下,设冒险者A使用了装备B:
装备B的类型 | 使用效果 | 输出文本 |
---|---|---|
Bottle(若 filled 为 true) | A的生命值增加[B的 capacity 属性]的十分之一,之后 B 的 filled 变为 false,price 变为原来的十分之一(向下取整)。 | {A 的 name} drank {B 的 name} and recovered {生命值增加量}. |
HealingPotion(若 filled为true) | A的生命值增加[B的 capacity 属性]的十分之一,之后 B 的 filled 变为 false,price 变为原来的十分之一(向下取整)。然后A的生命值再额外增加[B的capacity属性]乘以[B的efficiency属性]的量。 | {A 的 name} drank {B 的 name} and recovered {生命值增加量}. {A 的 name} recovered extra {生命值额外增加量}. |
ExpBottle(若 filled 为 true) | A的生命值增加[B的 capacity 属性]的十分之一,之后 B 的 filled 变为 false,price 变为原来的十分之一(向下取整)。然后A的经验值变为原来的[B的expRatio属性]倍。 | {A 的 name} drank {B 的 name} and recovered {生命值增加量}. {A 的 name}'s exp became {A 变化后的经验}. |
Bottle/HealingPotion/ExpBottle(若filled为false) | 无任何作用效果。 | Failed to use {B 的 name} because it is empty. |
Sword | 使用后A的生命值减少 10.0、经验值增加 10.0,金钱数增加相当于[B 的 sharpness属性]一倍的量。 | {A 的 name} used {B 的 name} and earned {增加的金钱数}. |
RareSword | 使用后A的生命值减少 10.0、经验值增加 10.0,金钱数增加相当于[B 的 sharpness属性]一倍的量。然后 A 的经验值额外增加[B 的 extraExpBonus 属性]。 | {A 的name} used {B 的name} and earned {增加的金钱数}. {A 的name} got extra exp {额外获得的经验}. |
EpicSword | 使用后A的生命值减少 10.0、经验值增加 10.0,金钱数增加相当于[B 的 sharpness属性]一倍的量。然后B的sharpness 属性变为原来的 evolveRatio倍。 | {A 的 name} used {B 的 name} and earned {增加的金钱数}. {B 的 name}'s sharpness became {B 变化后的sharpness}. |
请注意打印文本中存在的换行。
输入/输出格式
第一行一个整数 m,表示操作的个数。
接下来的 m 行,每行一个形如 {type} {attribute}
的操作,操作输入形式及其含义如下:
- 指令相较于 task3 增加两条,其余指令不发生变化。
type | attribute | 意义 | 输出文本 |
---|---|---|---|
8 | {adv_id} |
编号为 {adv_id} 的冒险者按照 price 由大到小的顺序使用其全部装备,若 price 相同则按照装备的 id 由大到小使用。( price 为所有装备本次使用前的 price ) |
每个装备在使用时会产生输出,除此之外无额外输出。 |
9 | {adv_id} |
打印编号为 {adv_id} 的冒险者的所有状态。 |
一个字符串表示冒险者的状态: The adventurer's id is {adv_id}, name is {name}, health is {health}, exp is {exp}, money is {money}. |
数据范围与操作限制
变量约束
变量 | 类型 | 说明 |
---|---|---|
id | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符 |
price | 长整数 | 在 long 精度范围内 |
capacity, efficiency, expRatio, sharpness, extraExpBonus, evolveRatio | 浮点数 | 在 double 精度范围内 |
操作约束
- 操作数满足 1≤m≤2000。
- 保证所有冒险者与装备的 ID 两两不同。
- 操作 1:不会加入与已有冒险者和装备 ID 相同 ID 的新冒险者。
- 操作 2:冒险者 ID 一定存在,且新装备的 ID 与当前所有冒险者和装备的 ID 均不相同。
- 操作 3:冒险者 ID 一定存在,且冒险者一定持有该 ID 的装备。
- 操作 4:冒险者 ID 一定存在,若冒险者不持有任何装备,则输出 0。
- 操作 5:冒险者 ID 一定存在,且冒险者一定持有至少一个装备。
- 操作 6:冒险者 ID 一定存在,若冒险者不持有任何装备,则输出 0。
- 操作 7:冒险者 ID 一定存在,且冒险者一定持有该 ID 的装备。
- 操作 8:冒险者 ID 一定存在。
- 操作 9:冒险者 ID 一定存在。
测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值 xout 和正确数值 xstd 之间的绝对或相对误差小于等于 10−5,则认为是相等的,即满足
输入样例
6
1 1 Person1
2 1 1 2 water_bottle1 10 50.0
2 1 6 3 epic_sword1 300 7.0 1.5
2 1 3 4 exp_bottle1 10 3.0 1.2
8 1
9 1
输出样例
Person1 used epic_sword1 and earned 7.0.
epic_sword1's sharpness became 10.5.
Person1 drank exp_bottle1 and recovered 0.3.
Person1's exp became 12.0.
Person1 drank water_bottle1 and recovered 5.0.
The adventurer's id is 1, name is Person1, health is 95.3, exp is 12.0, money is 7.0.
提示
- 建议在Equipment类中定义一个use方法,在所有的装备子类中都去重写这个use方法,另外还应该为所有需要打印描述字符串的类重写toString方法。在Adventurer类中定义
HashMap<Integer, Equipment>
类型的equipments属性,表示冒险者拥有的全部装备,在执行操作8时,先对equipments中存储的装备进行排序,然后依次调用这些装备对象的use方法(因为有多态机制,这里不需要强制转型,直接调用就可以保证行为正确)。 - 冒险者使用装备的过程中,是对冒险者属性和装备自身属性的读取,运算和修改。如何才能让装备类的方法可以读取并修改他的使用者的属性呢?为
use
方法传递一个冒险者作为参数是一个好主意。 - 在
Bottle
和它的子类在filled
为false
时被使用就可以看作是一种异常行为。于是你可以在Bottle.use
方法中抛出一个异常(使用 throw 语句),在HealingPotion.use
调用Bottle.use
时,不处理这个异常而是将其继续抛出到上层,而在冒险者循环使用装备的代码中将其捕获并打印出错误信息。
下面的代码是标程中Bottle类内定义的use方法。
@Override
public void use(Adventurer user) throws Exception {
if (!isFilled()) {
throw new Exception("Failed to use " + getName() + " because it is empty.");
}
user.setHealth(user.getHealth() + capacity / 10);
setFilled(false);
setPrice(getPrice().divide(BigInteger.TEN));
System.out.println(user.getName() +
" drank " + getName() +
" and recovered " + capacity / 10 +
".");
}
下面的代码是标程中在Adventurer类中用于完成操作8所定义的useAll方法
public void useAll() {
ArrayList<Equipment> sorted = new ArrayList<>(equipments.values());
Collections.sort(sorted, Comparator.reverseOrder());
for (Equipment equipment : sorted) {
try {
equipment.use(this);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
- 本次作业将会涉及到自定义排序,请学习如何给对象编写
compareTo
方法并实现Comparable
接口,之后即可利用Collections.sort
方法来给容器内对象进行排序,考虑到有许多知识同学们还没有学过,本章结尾会给出一个例子,同学们可以照猫画虎地完成,compareTo方法仅需要在Equipment类中定义,Equipment类的子类如果不重写该方法的话,将会与父类行为保持一致。
与
Collections.sort
会调用compareTo
方法实现自定义排序,类似地,TreeSet
和TreeMap
容器也会通过调用对象的compareTo
方法,从而维护一个key对象有序的集合/映射。另外,
HashSet
和HashMap
这两种容器会通过调用对象的hashCode
方法和equals
方法来将任意对象作为key来使用。这个知识点非常重要,请同学们务必弄懂其原理。Java中许多内置的类,比如
Integer
和BigInteger
等等都已经实现了compareTo
、hashCode
、equals
方法,所以你才可以直接把他们当作TreeMap
和HashMap
的key来使用。
// Comparable接口的例子
import java.util.ArrayList;
import java.util.Collections;
class MainClass {
public static void main(String[] args) {
Score xiaoMing = new Score(120, 138);
Score xiaoHong = new Score(125, 133);
Score xiaoWang = new Score(119, 145);
ArrayList<Score> scores = new ArrayList<>();
scores.add(xiaoMing);
scores.add(xiaoHong);
scores.add(xiaoWang);
Collections.sort(scores);
for (Score score : scores) { // 如果你使用IDEA编写代码,可以试一试打出 "scores.for<TAB>" 这一串按键,快速补齐for循环
System.out.println(score); // 试一试 "score.sout<TAB>",自动补齐打印语句
}
/*
运行结果如下,越大的对象越在后面(即升序排序):
Score{chinese=120, math=138}
Score{chinese=125, math=133}
Score{chinese=119, math=145}
*/
}
}
class Score implements Comparable<Score> { // 后面尖括号里的类名基本上都会与前面的类名相同,表达“Score这个类可以与Score这个类相比较”这个意思。
private final int chinese;
private final int math;
public Score(int chinese, int math) {
this.chinese = chinese;
this.math = math;
}
public int getSum() {
return chinese + math;
}
/**
* 自定义分数的比较规则,首先比较总分,总分相同比较语文,语文相同比较数学……
*/
@Override
public int compareTo(Score other) {
if (this.getSum() < other.getSum()) { //首先比较总分,总分高的先录取
return -1; // 返回 -1 代表 this 小于 other
} else if (this.getSum() > other.getSum()) {
return 1; // 返回 1 代表 this 大于 other
}
if (this.chinese < other.chinese) { //若总分一样,按语文分更高的先录取
return -1;
} else if (this.chinese > other.chinese) {
return 1;
}
// 返回任何负值都代表this < other,于是可以这样子简写,
// 下面三行关于math的比较和上面的五行关于chinese的比较是完全等价的。
if (this.math != other.math) {
return this.math - other.math;
}
return 0; //返回0代表两被比较对象相等
}
@Override
public String toString() {
return "Score{" +
"chinese=" + chinese +
", math=" + math +
'}';
}
}
值得注意的点
- 对于公有方法,设计时可以自上向下,有利于在顶层统一规范。
- 通过设计类内变量为
private
类型,并通过设计public
的get
和set
方法达到对其的访问是一种好的设计习惯。 - 对于本task,可以支持
Equipment
类子类对象之间的相互比较,因此可以使Equipment
实现Comparable
接口,并具体重写compareTo
方法,这将在下面扩展知识具体介绍。 HashSet
和HashMap
中的hashCode
和equals
方法,这将在下面扩展知识具体介绍。
扩展知识
Comparable
接口和compareTo
方法
Comparable
接口的声明为:
public interface Comparable<T> {
public int compareTo(T o);
}
当我们需要两个对象彼此可比较时,可以对其类实现该接口,并重写compareTo
方法,举例如下:
public class Equipment implements Comparable<Equipment> {
private long price;
public Equipment(long price) {
this.price = price;
}
public long getPrice() {
return price;
}
public void setPrice(long price) {
this.price = price;
}
@Override
public int compareTo(Equipment other) {
if (this.price > other.price) {
return 1;
} else if (this.price < other.price) {
return -1;
} else {
if (this.id > other.id) {
return 1;
} else {
return -1;
}
}
}
}
compareTo
方法返回值类型为int
,本意是希望当当前对象比传入参数对象大的时候返回正数,小的时候返回负数,相等返回0,但是实际应用中也可以直接逆过来实现降序。
但是笔者仍旧认为,对两个对象大小的比较在compareTo
中最好仍按照其本意写,此时若直接调用Collections.sort
则为升序,若需要降序则可以添加参数Comparator.reverseOrder()
,如:Collections.sort(sorted, Comparator.reverseOrder());
,其中sorted
为被比较对象类型的ArrayList
。
HashSet
和HashMap
与hashCode
和equals
abstractMap
类中有hashCode
方法和equals
方法,其原型如下:
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
hashCode
方法会返回一个哈希值。
equals
方法判断两个对象是否同一,并非两个对象是否相等。如果想要实现判断两个对象是否相等,需要自己重写方法。
由于指导书中特地强调了这两个方法的重要性,因此笔者将其列在这,但是目前不很清楚课程组的意图,等待未来更新.jpg
Part 5 Pre 2 task 5
题目
基本要求
在本任务中,我们允许冒险者雇佣并使用另一个冒险者,且赋予冒险者价值的概念,把装备和冒险者都看作是价值体。
题目描述
- 在 Task4 的基础上,我们定义冒险者的价值
price
为其拥有的所有价值体的价值之和,即冒险者的价值是其装备的价值及其雇佣的冒险者的价值的和。 - 在 Task4 的基础上,增加冒险者之间的雇佣关系:冒险者 A 雇佣冒险者 B,可以认为是把冒险者 B 看成一种价值体。此时冒险者 A 拥有价值体冒险者 B,之后冒险者 A 便可以像使用其他装备一样使用冒险者 B。
- 在Task4的基础上,定义冒险者 A 使用冒险者 B,其效果为冒险者 A 按照价值从大到小、价值相同则按价值体
id
从大到小的顺序(同 Task4) 依次使用冒险者 B 的价值体,价值体的价值指的是所有价值体在本次使用前的价值。我们规定:如果当前使用到了冒险者 B 雇佣的冒险者 C,则冒险者 C 要按照如上顺序使用其拥有的价值体,这些价值体将作用于最开始使用的冒险者,在此处即为冒险者 A。
输入/输出格式
第一行一个整数 m,表示操作的个数。
接下来的 m 行,每行一个形如 {type} {attribute}
的操作,操作输入形式及其含义如下:
- 对 Task4 中的一些指令进行少量修改,重点地方已经加粗,并新增一条指令 10:
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 由大到小的顺序使用。( 价值体价值为所有价值体本次使用前的价值)如果当前使用的是冒险者价值体,则按照上述顺序使用该冒险者价值体拥有的价值体 |
每个价值体在使用时就会产生输出,除此之外无额外输出 |
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 的冒险者 |
无 |
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 | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符 |
price | 长整数 | 在 long 精度范围内 |
capacity, efficiency, expRatio, sharpness, extraExpBonus, evolveRatio | 浮点数 | 在 double 精度范围内 |
操作约束
- 操作数满足 1≤m≤2000。
- 保证所有价值体的 ID 两两不同。
- 操作 1:不会加入与已有价值体 ID 相同 ID 的新冒险者。
- 操作 2:冒险者 ID 一定存在,且新装备的 ID 与当前所有价值体的 ID 均不相同。
- 操作 3:冒险者 ID 一定存在,且冒险者一定持有该 ID 的价值体。
- 操作 4:冒险者 ID 一定存在,若冒险者不持有任何价值体,则输出 0。
- 操作 5:冒险者 ID 一定存在,且冒险者一定持有至少一个价值体。
- 操作 6:冒险者 ID 一定存在,若冒险者不持有任何价值体,则输出 0。
- 操作 7:冒险者 ID 一定存在,且冒险者一定持有该 ID 的价值体。
- 操作 8:冒险者 ID 一定存在。
- 操作 9:冒险者 ID 一定存在。
- 操作 10:雇佣和被雇佣的冒险者均已存在,且不是同一个冒险者。
- 冒险者的雇佣关系不会存在循环雇佣的情况,每个冒险者最多仅能被一个其他冒险者雇佣一次。
测评方法
输出数值时,你的输出数值需要和正确数值相等。
假设你的输出值 xout 和正确数值 xstd 之间的绝对或相对误差小于等于 10−5,则认为是相等的,即满足
输入样例
13
1 1 Saber
1 2 Lancer
1 114514 Ausar
2 1 1 3 bottle1 3 3
2 2 4 4 sword1 4 5
2 2 4 5 sword2 10 2
2 114514 1 6 bottle2 3 3
10 1 2
10 2 114514
7 1 2
4 1
6 1
8 1
输出样例
The adventurer's id is 2, name is Lancer, health is 100.0, exp is 0.0, money is 0.0.
20
2
Saber used sword2 and earned 2.
Saber used sword1 and earned 5.
Saber drank bottle2 and recovered 0.3.
Saber drank bottle1 and recovered 0.3.
提示
冒险者和装备都是价值体,都可以求价值、被使用以及字符串化等,故一个推荐的设计方法是建立价值体接口 ,接口中包含上述提到的三个方法,让冒险者 Adventurer
和装备 Equipment
都实现这个接口,这样在顶层逻辑中就只能看到价值体这一种类型,可使用该类型的引用去调用不同子类型对象的这三种方法,这种处理称为归一化处理,会在正式课程中有专门的论述和训练。
值得注意的点
笔者在本次task进行了相当程度的重构和优化,代码结构和风格有较大提升,以下记录重构重点:
-
面向对象是一种思想,具体到底层单个方法的实现,依旧是面向过程的程序,在方法体中依旧不能抛弃结构化编程思想。举例来说,在
MainClass
的main
方法中,对于每种输入情况的判断如果不加以封装,则会非常冗长,一方面不符合结构化编程的思想,另一方面没办法通过checkstyle
中的一个要求——单个方法不超过60行。以下举例我的解决办法:
MainClass中对每个情况写了一个casex
函数(x=1,2,...10),分别处理不同情况,做到高内聚低耦合:
对于一个case
方法中情况较多的,同样再次细分方法:
值得注意的是,上述方法中均为static
方法,这样才可以在main
方法中被调用,因为main
方法也是static
的。
- 对具有公共行为的类,设置顶层接口并使得这些类实现这个接口以实现设计的统一。接口是一种抽象的类,但是其与继承并不相同,具体而言,在本task中,
Bottle
父类有ExpBottle
子类和HealingPotion
子类,子类拥有父类的全部特征和方法;Equipment
和Asventurer
宏观上来讲是不同的事物,但是它们都具有作为价值体的共同特征:如求价值等公共方法,因此可以用一个顶层公共接口约束其行为,在两个类中分别实现接口。继承和接口的区别可以在未来的代码过程中进一步体会。 - 在本task中,有一个细节值得注意:若冒险者
useall
,该冒险者还雇佣了冒险者,那么被雇佣的冒险者的价值体的使用应当被归到雇佣其的冒险者身上。 - 在本task中涉及了异常相关概念,可以在未来代码过程中更进一步体会理解。
Part 6 后记
本篇博客字数略多,其中题目描述占了大部分,笔者心得和体会篇幅并不很长。博客中仍有一些悬而未决的问题,如课程组强调hashCode
和equals
方法的意图是什么等,如果有所体会将会回来更新。
__EOF__