「BUAA OO Pre」 Pre 2總結回顧概覽

被水淹沒的一條魚發表於2022-02-26

「BUAA OO Pre」 Pre 2總結回顧概覽

Part 0 前言

寫作背景

筆者在完成寒假預習作業Pre2系列任務時遇到了一些挑戰並有一些收穫和心得,在這裡記錄和大家分享。

定位

基於本篇部落格,您可以瞭解筆者在實現Pre2尤其是迭代開發中的心路歷程及踩過的坑。

為了讀者的閱讀體驗,本篇部落格將按照task順序展開。

為了部落格的完整性和順利展開,對於課程組guidebook中的題目要求部分將做引用,若有侵權請聯絡即刻刪除相關內容。

您可以在這裡期望獲得

  1. 筆者從做題者的視角的收穫
  2. 筆者踩過的坑

您在這裡無法期望獲得

  1. 部分知識的深層原理

對讀者前置知識的期望

  1. 完成《程式設計基礎》/《C語言程式設計》和《資料結構》課程
  2. 有一般學生C語言的水平
  3. 瞭解Java語言的基本語法,此部分知識可以通過各種線上資源獲得

Part 1 Pre 2 task 1

題目

描述

先介紹 pre2 練習的背景故事。

想象你是一個冒險者,現在正在一個新的星球上進行探險,這個過程中你需要通過努力收集各種物品來不斷增強自身能力值,在第一個 task 中你需要對第一個基本物品 Bottle 進行建模。

經過之前的預習和上面的練習現在到了實戰的時候了:構造一個 Bottle 類,來表示冒險者需要用到的瓶子類,要求 Bottle 類包含屬性:ID,名字,價格,容量,和表達瓶子是否裝滿的標誌量。相應的要求完成的程式可以查詢瓶子的 ID,名字,價格,容量以及是否裝滿。

一開始給出一個瓶子。之後,有 7 種操作:(序號即為運算元)

  1. 查詢名字
  2. 查詢價格
  3. 查詢容量
  4. 查詢是否裝滿
  5. 更改價格
  6. 設定是否裝滿
  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 \leq m \leq 2000\)​。

測評方法

輸出數值時,你的輸出數值需要和正確數值相等。

假設你的輸出值 \(x_{out}\) 和正確數值 \(x_{std}\) 之間的絕對或相對誤差小於等於 \(10 ^ {-5}\),則認為是相等的,即滿足

\[\dfrac {|x_{std} - x_{out}|}{\max(1, |x_{std}|)} \leq 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 系列任務的迭代形式:

Task1Task3 ,逐步引導同學們實現一系列基礎的類,並且熟悉類、屬性和方法的使用,引導大家向物件導向的思維方式轉變。

Task4 涉及方法的重寫和複用,並引入異常處理機制,希望同學們可以感性地體會到層次化設計的好處,瞭解並簡單應用異常處理(異常處理在之後也常會用到)。

Task5 涉及介面,需要同學們在之前 Task 的基礎上完成更加複雜的操作。如果此時仍然使用原來的編碼習慣,會在這個 Task 中遇到巨大困難,而嚴格按照我們的提示去做的同學會體會到好處。

強烈建議按照從 Task1~Task5 的順序完成 pre2 的練習,思考如何進行增量迭代和持續重構,而不要在實現每一個 Task 時都完成一份新的程式碼。

值得注意的點

  1. 讀者應當先按照pre1中工具鏈相關介紹完整正確配置好所有相關配置,特別是IDEA的checkstyle及new file settings等,磨刀不誤砍柴工。
  2. 建議每一個類(class)都新建一個.java檔案,一方面保證高內聚低耦合,另一方面使得結構層次清晰。
  3. 站在Pre 2 task 5的視角來看,從這裡開始就應當逐漸學習並掌握靜態方法靜態變數的書寫方式,以在未來更復雜的輸入情況中依然可以遵守checkstyle要求的類行數不超過60的要求。
  4. 時刻記得checkstyle,以保證程式碼風格符合規範。
  5. 使用package管理當前作業的檔案。
  6. commit到課程倉庫的只需要所有.java檔案,不需要其他檔案。

Part 2 Pre 2 task 2

題目

基本要求

  • 建立冒險者類,且符合封裝的要求
  • 使用適當的容器管理多個冒險者例項

描述

在這個問題中,你需要管理多個冒險者。初始時,你沒有需要管理的冒險者。接下來會有 \(m\) 個操作:

  1. 加入一個需要管理的冒險者
  2. 給某個冒險者增加一個瓶子
  3. 刪除某個冒險者的一個瓶子
  4. 查詢某個冒險者所持有瓶子的價格之和
  5. 查詢某個冒險者所持有瓶子價格的最大值

你需要對操作 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 \leq m \leq 2000\)​。
  • 保證所有冒險者與瓶子的 ID 兩兩不同。
  • 操作 1:不會加入與已有冒險者和瓶子 ID 相同 ID 的新冒險者。
  • 操作 2:冒險者 ID 一定存在,且新瓶子的 ID 與當前所有冒險者和瓶子的 ID 均不相同。
  • 操作 3:冒險者 ID 一定存在,且冒險者一定持有該 ID 的瓶子。
  • 操作 4:冒險者 ID 一定存在,若冒險者不持有任何瓶子,則輸出 0。
  • 操作 5:冒險者 ID 一定存在,且冒險者一定持有至少一個瓶子。

測評方法

輸出數值時,你的輸出數值需要和正確數值相等。

假設你的輸出值 \(x_{out}\) 和正確數值 \(x_{std}\) 之間的絕對或相對誤差小於等於 \(10 ^ {-5}\),則認為是相等的,即滿足

\[\dfrac {|x_{std} - x_{out}|}{\max(1, |x_{std}|)} \leq 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 中有些特別的類用於處理大數運算,如 BigIntegerBigDecimal


值得注意的點

  1. 瞭解BigInteger相關知識並應用,這將在下面的擴充套件知識進行部分介紹。

  2. 瞭解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");

常用方法
  1. 加減乘除:

    加法:BigInteger add(BigInteger val)

    減法:BigInteger subtract(BigInteger val)

    乘法:BigInteger multiply(BigInteger val)

    除法:BigInteger divide(BigInteger val)

  2. 獲得兩個BigInteger中的最大/最小值

    最大值:BigInteger max(BigInteger val)

    最小值:BigInteger min(Biginteger val)

  3. 獲得(長)整型(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 為浮點數型別

將有以下操作:

  1. 加入一個冒險者
  2. 給某個冒險者新增某件裝備(裝備包括藥水和武器)
  3. 刪除某個冒險者擁有的某個裝備
  4. 查詢某個冒險者所擁有裝備的價格之和
  5. 查詢某個冒險者所擁有裝備的價格最大值
  6. 查詢某個冒險者擁有的裝備總數
  7. 列印一個裝備的全部屬性,屬性的輸出順序與輸入建立該裝備時給定的各引數順序一致,具體格式詳見下方屬性列印方式

輸入輸出

第一行一個整數 \(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 \leq m \leq 2000\)​。
  • 保證所有冒險者與裝備的 ID 兩兩不同。
  • 操作 1:不會加入與已有冒險者和裝備 ID 相同 ID 的新冒險者。
  • 操作 2:冒險者 ID 一定存在,且新裝備的 ID 與當前所有冒險者和裝備的 ID 均不相同。
  • 操作 3:冒險者 ID 一定存在,且冒險者一定持有該 ID 的裝備。
  • 操作 4:冒險者 ID 一定存在,若冒險者不持有任何裝備,則輸出 0。
  • 操作 5:冒險者 ID 一定存在,且冒險者一定持有至少一個裝備。
  • 操作 6:冒險者 ID 一定存在,若冒險者不持有任何裝備,則輸出 0。
  • 操作 7:冒險者 ID 一定存在,且冒險者一定持有該 ID 的裝備。

測評方法

輸出數值時,你的輸出數值需要和正確數值相等。

假設你的輸出值 \(x_{out}\) 和正確數值 \(x_{std}\) 之間的絕對或相對誤差小於等於 \(10 ^ {-5}\),則認為是相等的,即滿足

\[\dfrac {|x_{std} - x_{out}|}{\max(1, |x_{std}|)} \leq 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

補充材料

  1. 請思考,本次作業中的求和操作等是否會出現超出資料限制的情況。

    提示:Java 中有特別的類:BigIntegerBigDecimal

  2. 設計模式是軟體開發人員經過相當長的實踐總結出來的最佳設計方案,在物件導向設計與構造課程中,你將逐步瞭解和掌握幾種基本的設計模式,包括工廠模式、單例模式、生產者-消費者模式等。

    現在,希望大家可以瞭解工廠模式,這是在繼承和介面實現中常用的設計模式。

    大家可以參考連結中的介紹,也可以自行查閱資料。這將幫助你更輕鬆的完成日後的作業 ?

值得注意的點

  1. IDEA去掉空白快捷鍵:Ctrl + Shift + J;格式化程式碼:Ctrl + Alt + L
  2. 子類繼承父類:extends關鍵字。
  3. ArrayListHashMap主要使用特點和區別,這將在下面的擴充套件知識具體介紹。
  4. 繼承中的向上轉型、向下轉型與方法重寫,這將在下面的擴充套件知識具體介紹。

擴充套件知識

ArrayListHashMap基礎用法對比
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 ArrayListJava 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型別的物件來引用objA 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 \leq m \leq 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 一定存在。

測評方法

輸出數值時,你的輸出數值需要和正確數值相等。

假設你的輸出值 \(x_{out}\) 和正確數值 \(x_{std}\) 之間的絕對或相對誤差小於等於 \(10 ^ {-5}\),則認為是相等的,即滿足

\[\dfrac {|x_{std} - x_{out}|}{\max(1, |x_{std}|)} \leq 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.

提示

  1. 建議在Equipment類中定義一個use方法,在所有的裝備子類中都去重寫這個use方法,另外還應該為所有需要列印描述字串的類重寫toString方法。在Adventurer類中定義HashMap<Integer, Equipment>型別的equipments屬性,表示冒險者擁有的全部裝備,在執行操作8時,先對equipments中儲存的裝備進行排序,然後依次呼叫這些裝備物件的use方法(因為有多型機制,這裡不需要強制轉型,直接呼叫就可以保證行為正確)。
  2. 冒險者使用裝備的過程中,是對冒險者屬性和裝備自身屬性的讀取,運算和修改。如何才能讓裝備類的方法可以讀取並修改他的使用者的屬性呢?為use方法傳遞一個冒險者作為引數是一個好主意。
  3. Bottle 和它的子類在 filledfalse 時被使用就可以看作是一種異常行為。於是你可以在 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());
        }
    }
}
  1. 本次作業將會涉及到自定義排序,請學習如何給物件編寫 compareTo 方法並實現 Comparable 介面,之後即可利用 Collections.sort 方法來給容器內物件進行排序,考慮到有許多知識同學們還沒有學過,本章結尾會給出一個例子,同學們可以照貓畫虎地完成,compareTo方法僅需要在Equipment類中定義,Equipment類的子類如果不重寫該方法的話,將會與父類行為保持一致。

Collections.sort 會呼叫 compareTo 方法實現自定義排序,類似地,TreeSetTreeMap 容器也會通過呼叫物件的 compareTo 方法,從而維護一個key物件有序的集合/對映。

另外,HashSetHashMap 這兩種容器會通過呼叫物件的 hashCode 方法和 equals 方法來將任意物件作為key來使用。這個知識點非常重要,請同學們務必弄懂其原理

Java中許多內建的類,比如 IntegerBigInteger 等等都已經實現了compareTohashCodeequals 方法,所以你才可以直接把他們當作 TreeMapHashMap 的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 +
                '}';
    }

}

值得注意的點

  1. 對於公有方法,設計時可以自上向下,有利於在頂層統一規範。
  2. 通過設計類內變數為private型別,並通過設計publicgetset方法達到對其的訪問是一種好的設計習慣。
  3. 對於本task,可以支援Equipment類子類物件之間的相互比較,因此可以使Equipment實現Comparable介面,並具體重寫compareTo方法,這將在下面擴充套件知識具體介紹。
  4. HashSetHashMap中的hashCodeequals方法,這將在下面擴充套件知識具體介紹。

擴充套件知識

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

HashSetHashMaphashCodeequals

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的冒險者

varsequipment_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 \leq m \leq 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:僱傭和被僱傭的冒險者均已存在,且不是同一個冒險者。
  • 冒險者的僱傭關係不會存在迴圈僱傭的情況,每個冒險者最多僅能被一個其他冒險者僱傭一次。

測評方法

輸出數值時,你的輸出數值需要和正確數值相等。

假設你的輸出值 \(x_{out}\) 和正確數值 \(x_{std}\) 之間的絕對或相對誤差小於等於 \(10 ^ {-5}\),則認為是相等的,即滿足

\[\dfrac {|x_{std} - x_{out}|}{\max(1, |x_{std}|)} \leq 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進行了相當程度的重構和優化,程式碼結構和風格有較大提升,以下記錄重構重點:

  1. 物件導向是一種思想,具體到底層單個方法的實現,依舊是程式導向的程式,在方法體中依舊不能拋棄結構化程式設計思想。舉例來說,在MainClassmain方法中,對於每種輸入情況的判斷如果不加以封裝,則會非常冗長,一方面不符合結構化程式設計的思想,另一方面沒辦法通過checkstyle中的一個要求——單個方法不超過60行。以下舉例我的解決辦法:

    main方法中按照輸入情況傳引數到分別的方法中:
    image

MainClass中對每個情況寫了一個casex函式(x=1,2,...10),分別處理不同情況,做到高內聚低耦合:
image

對於一個case方法中情況較多的,同樣再次細分方法:

image
image

值得注意的是,上述方法中均為static方法,這樣才可以在main方法中被呼叫,因為main方法也是static的。

  1. 具有公共行為的類,設定頂層介面並使得這些類實現這個介面以實現設計的統一。介面是一種抽象的類,但是其與繼承並不相同,具體而言,在本task中,Bottle父類有ExpBottle子類和HealingPotion子類,子類擁有父類的全部特徵和方法;EquipmentAsventurer巨集觀上來講是不同的事物,但是它們都具有作為價值體的共同特徵:如求價值等公共方法,因此可以用一個頂層公共介面約束其行為,在兩個類中分別實現介面。繼承和介面的區別可以在未來的程式碼過程中進一步體會。
  2. 在本task中,有一個細節值得注意:若冒險者useall,該冒險者還僱傭了冒險者,那麼被僱傭的冒險者的價值體的使用應當被歸到僱傭其的冒險者身上。
  3. 在本task中涉及了異常相關概念,可以在未來程式碼過程中更進一步體會理解。

Part 6 後記

本篇部落格字數略多,其中題目描述佔了大部分,筆者心得和體會篇幅並不很長。部落格中仍有一些懸而未決的問題,如課程組強調hashCodeequals方法的意圖是什麼等,如果有所體會將會回來更新。

相關文章