這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。
簡介
在本程式碼研究室中,您會建立 Kotlin 程式,並瞭解 Kotlin 中的類別和物件。如果您熟悉其他物件導向語言,應該會覺得這門課程的許多內容似曾相識,但 Kotlin 有一些重要差異,可減少您需要編寫的程式碼量。您也會瞭解抽象類別和介面委派。
本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程。
必備知識
- Kotlin 的基本概念,包括型別、運算子和迴圈
- Kotlin 的函式語法
- 物件導向程式設計基礎知識
- IntelliJ IDEA 或 Android Studio 等 IDE 的基本概念
課程內容
- 如何在 Kotlin 中建立類別及存取屬性
- 如何在 Kotlin 中建立及使用類別建構函式
- 如何建立子類別,以及繼承的運作方式
- 關於抽象類別、介面和介面委派
- 如何建立及使用資料類別
- 如何使用單例、列舉和密封類別
學習內容
- 建立包含屬性的類別
- 建立類別的建構函式
- 建立子類別
- 查看抽象類別和介面的範例
- 建立簡單的資料類別
- 瞭解單例項、列舉和密封類別
您應該已熟悉下列程式設計用語:
- 類別是物件的藍圖,舉例來說,
Aquarium
類別是製作水族箱物件的藍圖。 - 物件是類別的執行個體,水族箱物件就是一個實際的
Aquarium
。 - 屬性是類別的特徵,例如
Aquarium
的長度、寬度和高度。 - 方法 (也稱為成員函式) 是類別的功能。方法是指您可對物件「執行」的動作。舉例來說,您可以
fillWithWater()
Aquarium
物件。 - 介面是類別可實作的規格。舉例來說,清潔是水族箱以外的物件常見的動作,而且不同物件的清潔方式通常類似。因此您可以建立名為
Clean
的介面,定義clean()
方法。Aquarium
類別可以實作Clean
介面,使用軟海綿清潔水族箱。 - 套件可將相關程式碼分組,方便整理或製作程式碼程式庫。建立套件後,您可以將套件內容匯入其他檔案,並重複使用其中的程式碼和類別。
在這項工作中,您會建立新套件和類別,並加入一些屬性和方法。
步驟 1:建立套件
套件可協助您整理程式碼。
- 在「Project」窗格中,於「Hello Kotlin」專案下方的「src」資料夾上按一下滑鼠右鍵。
- 依序選取「New」(新增) >「Package」(套件),然後將其命名為
example.myapp
。
步驟 2:建立包含屬性的類別
類別是以 class
關鍵字定義,且類別名稱慣例會以大寫字母開頭。
- 在「example.myapp」example.myapp套件上按一下滑鼠右鍵。
- 依序選取「New」>「Kotlin File / Class」。
- 在「Kind」下方選取「Class」,然後將類別命名為
Aquarium
。IntelliJ IDEA 會在檔案中加入套件名稱,並為您建立空白的Aquarium
類別。 - 在
Aquarium
類別中,定義並初始化寬度、高度和長度 (以公分為單位) 的var
屬性。使用預設值初始化屬性。
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
在幕後,Kotlin 會自動為您在 Aquarium
類別中定義的屬性建立 getter 和 setter,因此您可以直接存取屬性,例如 myAquarium.length
。
步驟 3:建立 main() 函式
建立名為 main.kt
的新檔案,用來保存 main()
函式。
- 在左側的「Project」窗格中,對「example.myapp」套件按一下滑鼠右鍵。
- 依序選取「New」>「Kotlin File / Class」。
- 在「類型」下拉式選單中,將選取項目保留為「檔案」,並將檔案命名為
main.kt
。IntelliJ IDEA 會包含套件名稱,但不會包含檔案的類別定義。 - 定義
buildAquarium()
函式,並在其中建立Aquarium
的例項。如要建立執行個體,請參照類別,就如同它是函式Aquarium()
一樣。這會呼叫類別的建構函式,並建立Aquarium
類別的例項,類似於在其他語言中使用new
。 - 定義
main()
函式並呼叫buildAquarium()
。
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
步驟 4:新增方法
- 在
Aquarium
類別中,新增方法來列印水族箱的維度屬性。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- 在
main.kt
的buildAquarium()
中,對myAquarium
呼叫printSize()
方法。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- 按一下「
main()
」函式旁邊的綠色三角形,即可執行程式。查看結果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- 在
buildAquarium()
中,新增程式碼將高度設為 60,並列印變更後的維度屬性。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- 執行程式並觀察輸出內容。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
在這項工作中,您將為類別建立建構函式,並繼續使用屬性。
步驟 1:建立建構函式
在這個步驟中,您會將建構函式新增至第一個工作建立的 Aquarium
類別。在先前的範例中,每個 Aquarium
例項都是以相同尺寸建立。建立後,您可以設定屬性來變更尺寸,但一開始就建立正確大小的尺寸會比較簡單。
在某些程式設計語言中,建構函式的定義方式是在類別中建立與類別同名的方法。在 Kotlin 中,您可以在類別宣告本身直接定義建構函式,並在括號內指定參數,就像類別是方法一樣。與 Kotlin 中的函式一樣,這些參數可以包含預設值。
- 在您先前建立的
Aquarium
類別中,變更類別定義,加入三個建構函式參數,並為length
、width
和height
指派預設值,然後將這些參數指派給對應的屬性。
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions in cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}
- 更精簡的 Kotlin 做法是使用
var
或val
,直接透過建構函式定義屬性,Kotlin 也會自動建立 getter 和 setter。接著,您就可以移除類別主體中的屬性定義。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- 使用該建構函式建立
Aquarium
物件時,您可以不指定任何引數並取得預設值,也可以只指定部分引數,或指定所有引數並建立完全自訂大小的Aquarium
。在buildAquarium()
函式中,嘗試使用具名參數建立Aquarium
物件的不同方式。
fun buildAquarium() {
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
}
- 執行程式並觀察輸出內容。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 25 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 110 cm Height: 35 cm Width: 25 cm Length: 110 cm Height: 35 cm
請注意,您不必多載建構函式,也不必為每種情況 (以及其他組合) 編寫不同版本。Kotlin 會根據預設值和具名參數建立所需項目。
步驟 2:新增初始化區塊
上述範例建構函式只會宣告屬性,並將運算式的值指派給這些屬性。如果建構函式需要更多初始化程式碼,可以放在一或多個 init
區塊中。在這個步驟中,您要在 Aquarium
類別中新增一些 init
區塊。
- 在
Aquarium
類別中,新增init
區塊來輸出物件正在初始化,並新增第二個區塊來輸出以公升為單位的容量。
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
init {
// 1 liter = 1000 cm^3
println("Volume: ${width * length * height / 1000} l")
}
}
- 執行程式並觀察輸出內容。
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm
請注意,系統會依 init
區塊在類別定義中的顯示順序執行這些區塊,且呼叫建構函式時會執行所有區塊。
步驟 3:瞭解次要建構函式
在本步驟中,您將瞭解次要建構函式,並將一個建構函式新增至類別。除了可包含一或多個 init
區塊的主要建構函式,Kotlin 類別也可以有一或多個次要建構函式,以允許建構函式多載,也就是具有不同引數的建構函式。
- 在
Aquarium
類別中,使用constructor
關鍵字新增次要建構函式,並將魚隻數量做為引數。根據魚隻數量,為以公升為單位的魚缸計算容量建立val
罐屬性。假設每條魚需要 2 公升 (2,000 立方公分) 的水,並預留一些空間,以免水溢出。
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}
- 在次要建構函式中,請保持長度和寬度 (在主要建構函式中設定) 不變,並計算所需高度,使水箱達到指定容量。
// calculate the height needed
height = (tank / (length * width)).toInt()
- 在
buildAquarium()
函式中,新增呼叫來使用新的次要建構函式建立Aquarium
。顯示大小和音量。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- 執行程式並觀察輸出內容。
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
請注意,音量會列印兩次,一次是由主要建構函式中的 init
區塊在執行次要建構函式前列印,另一次是由 buildAquarium()
中的程式碼列印。
您也可以在主要建構函式中加入 constructor
關鍵字,但大多數情況下不需要這麼做。
步驟 4:新增屬性 Getter
在這個步驟中,您要新增明確的屬性 Getter。定義屬性時,Kotlin 會自動定義 getter 和 setter,但有時需要調整或計算屬性的值。舉例來說,您在上方列印了 Aquarium
的體積。您可以定義變數和 getter,將音量設為屬性。由於 volume
需要計算,getter 必須傳回計算值,你可以使用單行函式執行這項操作。
- 在
Aquarium
類別中,定義名為volume
的Int
屬性,並在下一行定義可計算音量的get()
方法。
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- 移除會列印音量的
init
區塊。 - 移除
buildAquarium()
中列印音量的程式碼。 - 在
printSize()
方法中,新增一行程式碼來列印音量。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- 執行程式並觀察輸出內容。
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
維度和體積與先前相同,但物件由主要建構函式和次要建構函式完整初始化後,體積只會列印一次。
步驟 5:新增屬性設定器
在這個步驟中,您將為磁碟區建立新的屬性設定器。
- 在
Aquarium
類別中,將volume
變更為var
,這樣就能設定多次。 - 在 getter 下方新增
set()
方法,為volume
屬性新增 setter,根據提供的水量重新計算高度。按照慣例,設定器參數的名稱為value
,但您也可以視需要變更。
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- 在
buildAquarium()
中新增程式碼,將水族箱的容量設為 70 公升。顯示新大小。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- 再次執行程式,並觀察高度和音量的變化。
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 l
到目前為止,程式碼中沒有任何瀏覽權限修飾符,例如 public
或 private
。這是因為 Kotlin 中的所有項目預設都是公開,也就是說,所有項目 (包括類別、方法、屬性和成員變數) 都可從任何位置存取。
在 Kotlin 中,類別、物件、介面、建構函式、函式、屬性及其 setter 可以有瀏覽權限修飾符:
public
表示在類別外可見。根據預設,所有項目 (包括類別的變數和方法) 都是公開的。internal
表示該函式只會在該模組內顯示。「模組」是一組一起編譯的 Kotlin 檔案,例如程式庫或應用程式。private
表示該函式只會顯示在該類別中 (或您使用函式時的來源檔案中)。protected
與private
相同,但任何子類別也都能看到。
詳情請參閱 Kotlin 說明文件中的「可視度修飾符」。
成員變數
類別中的屬性或成員變數預設為 public
。如果您使用 var
定義這些類別,這些類別就是可變動的,也就是可讀取及寫入。如果使用 val
定義,初始化後即為唯讀。
如果您希望程式碼可以讀取或寫入屬性,但外部程式碼只能讀取,可以將屬性和 getter 設為公開,並將 setter 宣告為私有,如下所示。
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
在這項工作中,您將瞭解 Kotlin 中的子類別和繼承機制。這些字詞與其他語言的字詞類似,但仍有部分差異。
在 Kotlin 中,類別預設無法加入子類別。同樣地,子類別無法覆寫屬性和成員變數 (但可以存取)。
您必須將類別標示為 open
,才能加入子類別。同樣地,您必須將屬性和成員變數標示為 open
,才能在子類別中覆寫這些項目。您必須使用 open
關鍵字,避免不慎將實作詳細資料洩漏為類別介面的一部分。
步驟 1:開放「水族館」類別
在這個步驟中,您會建立 Aquarium
類別 open
,以便在下一個步驟中覆寫該類別。
- 為
Aquarium
類別及其所有屬性標記open
關鍵字。
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- 新增開放式
shape
屬性,值為"rectangle"
。
open val shape = "rectangle"
- 新增具有 getter 的公開
water
屬性,該屬性會傳回Aquarium
音量的 90%。
open var water: Double = 0.0
get() = volume * 0.9
- 在
printSize()
方法中新增程式碼,列印形狀和水量 (以體積百分比表示)。
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
- 在
buildAquarium()
中,變更程式碼以建立含有width = 25
、length = 25
和height = 40
的Aquarium
。
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- 執行程式並觀察新的輸出內容。
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
步驟 2:建立子類別
- 建立名為
TowerTank
的Aquarium
子類別,實作圓柱形水箱,而非矩形水箱。您可以在Aquarium
下方新增TowerTank
,因為您可以在與Aquarium
類別相同的檔案中新增另一個類別。 - 在
TowerTank
中,覆寫建構函式中定義的height
屬性。如要覆寫屬性,請在子類別中使用override
關鍵字。
- 讓
TowerTank
的建構函式接受diameter
。在Aquarium
父類別中呼叫建構函式時,請同時使用diameter
做為length
和width
。
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- 覆寫 volume 屬性,計算圓柱體積。圓柱體的公式為圓周率乘以半徑的平方,再乘以高。您需要從
java.lang.Math
匯入常數PI
。
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
- 在
TowerTank
中,將water
屬性覆寫為音量的 80%。
override var water = volume * 0.8
- 將
shape
覆寫為"cylinder"
。
override val shape = "cylinder"
- 最終的
TowerTank
類別應如下所示。
Aquarium.kt
:
package example.myapp
import java.lang.Math.PI
... // existing Aquarium class
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}
- 在
buildAquarium()
中建立TowerTank
,直徑為 25 公分,高度為 45 公分。顯示大小。
main.kt:
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}
- 執行程式並觀察輸出內容。
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 l Water: 14.4 l (80.0% full)
有時您會想定義通用行為或屬性,以便在某些相關類別之間共用。Kotlin 提供兩種方法:介面和抽象類別。在這項工作中,您將為所有魚類共有的屬性建立抽象 AquariumFish
類別。您建立名為 FishAction
的介面,定義所有魚類通用的行為。
- 抽象類別和介面都無法自行例項化,也就是說,您無法直接建立這些型別的物件。
- 抽象類別有建構函式。
- 介面無法包含任何建構函式邏輯或儲存任何狀態。
步驟 1:建立抽象類別
- 在 example.myapp 下方,建立新檔案
AquariumFish.kt
。 - 建立名為
AquariumFish
的類別,並標示為abstract
。 - 新增一個
String
屬性 (color
),並標示abstract
。
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- 建立
AquariumFish
的兩個子類別:Shark
和Plecostomus
。 - 由於
color
是抽象類別,因此子類別必須實作。將Shark
設為灰色,Plecostomus
設為金色。
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- 在 main.kt 中,建立
makeFish()
函式來測試類別。例項化Shark
和Plecostomus
,然後列印每個項目的顏色。 - 刪除
main()
中先前的測試程式碼,並新增對makeFish()
的呼叫。程式碼應如下所示。
main.kt
:
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}
- 執行程式並觀察輸出內容。
⇒ Shark: gray Plecostomus: gold
下圖代表 Shark
類別和 Plecostomus
類別,這兩個類別會將抽象類別 AquariumFish
子類別化。
步驟 2:建立介面
- 在 AquariumFish.kt 中,建立名為
FishAction
的介面,其中包含eat()
方法。
interface FishAction {
fun eat()
}
- 在每個子類別中新增
FishAction
,並實作eat()
,讓它輸出魚的動作。
class Shark: AquariumFish(), FishAction {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- 在
makeFish()
函式中,呼叫eat()
,讓您建立的每條魚吃東西。
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- 執行程式並觀察輸出內容。
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
下圖代表 Shark
類別和 Plecostomus
類別,這兩個類別都由 FishAction
介面組成並實作該介面。
抽象類別與介面的使用時機
上述範例很簡單,但如果您有許多相互關聯的類別,抽象類別和介面可協助您保持設計簡潔、更有條理,且更容易維護。
如上所述,抽象類別可以有建構函式,但介面不行,除此之外,兩者非常相似。那麼,您應該在什麼時候使用哪一種呢?
使用介面組合類別時,類別所含的類別例項會擴充類別的功能。相較於從抽象類別繼承,組合通常更容易重複使用程式碼和進行推理。此外,您可以在類別中使用多個介面,但只能從一個抽象類別建立子類別。
組合通常可帶來更完善的封裝、更低的耦合 (相互依賴)、更簡潔的介面,以及更實用的程式碼。基於這些原因,使用介面組合是較好的設計。另一方面,從抽象類別繼承往往是某些問題的自然解決方式。因此您應優先使用組合,但如果繼承有意義,Kotlin 也允許您這麼做!
- 如果您有很多方法和一或兩種預設實作項目,請使用介面,例如下方的
AquariumAction
。
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- 只要無法完成類別,就可以使用抽象類別。舉例來說,回到
AquariumFish
類別,您可以讓所有AquariumFish
實作FishAction
,並為eat
提供預設實作,同時將color
設為抽象,因為魚類沒有預設顏色。
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
先前的練習介紹了抽象類別、介面和組合的概念。介面委派是一種進階技術,介面的方法是由輔助 (或委派) 物件實作,然後由類別使用。在一連串不相關的類別中使用介面時,這個技巧就非常實用:您可以在個別的輔助類別中新增所需的介面功能,而每個類別都會使用輔助類別的執行個體來實作功能。
在這項工作中,您會使用介面委派將功能新增至類別。
步驟 1:建立新介面
- 在 AquariumFish.kt 中,移除
AquariumFish
類別。Plecostomus
和 不會從AquariumFish
類別繼承,而是會實作魚類動作和顏色的介面。Shark
- 建立新的介面
FishColor
,將顏色定義為字串。
interface FishColor {
val color: String
}
- 變更
Plecostomus
,實作兩個介面FishAction
和FishColor
。您需要從FishColor
覆寫color
,並從FishAction
覆寫eat()
。
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- 將
Shark
類別變更為同時實作FishAction
和FishColor
這兩個介面,而非從AquariumFish
繼承。
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- 完成的程式碼應如下所示:
package example.myapp
interface FishAction {
fun eat()
}
interface FishColor {
val color: String
}
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
步驟 2:建立單例類別
接著,請建立導入 FishColor
的輔助程式類別,實作委派部分的設定。您建立名為 GoldColor
的基本類別,實作 FishColor
,只會說明其顏色為金色。
建立多個 GoldColor
執行個體沒有意義,因為這些執行個體執行的動作完全相同。因此,Kotlin 可讓您宣告類別,且只能使用 object
關鍵字 (而非 class
) 建立一個例項。Kotlin 會建立該執行個體,而該執行個體會由類別名稱參照。然後所有其他物件都可以只使用這個例項,因為無法建立這個類別的其他例項。如果您熟悉單例模式,這就是在 Kotlin 中實作單例模式的方式。
- 在 AquariumFish.kt 中,為
GoldColor
建立物件。覆寫顏色。
object GoldColor : FishColor {
override val color = "gold"
}
步驟 3:為 FishColor 新增介面委派
現在您已準備好使用介面委派。
- 在 AquariumFish.kt 中,從
Plecostomus
移除color
的覆寫。 - 變更
Plecostomus
類別,從GoldColor
取得顏色。方法是在類別宣告中加入by GoldColor
,然後建立委派。這表示請使用GoldColor
提供的實作項目,而非實作FishColor
。因此每次存取color
時,都會委派給GoldColor
。
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
以目前的類別來說,所有異形魚都會是金色的,但這些魚其實有許多顏色。如要解決這個問題,請為顏色新增建構函式參數,並將 GoldColor
設為 Plecostomus
的預設顏色。
- 變更
Plecostomus
類別,以透過建構函式採用傳入的fishColor
,並將預設值設為GoldColor
。將委派項目從by GoldColor
變更為by fishColor
。
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}
步驟 4:為 FishAction 新增介面委派
同樣地,您也可以對 FishAction
使用介面委派。
- 在 AquariumFish.kt 中,建立實作
FishAction
的PrintingFishAction
類別,該類別會採用String
和food
,然後輸出魚類吃的食物。
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- 在
Plecostomus
類別中,移除覆寫函式eat()
,因為您將以委派項目取代該函式。 - 在
Plecostomus
的宣告中,將FishAction
委派給PrintingFishAction
,並傳遞"eat algae"
。 - 完成所有委派作業後,
Plecostomus
類別主體中就不會有任何程式碼,因此請移除{}
,因為所有覆寫作業都會由介面委派處理
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
下圖代表 Shark
和 Plecostomus
類別,兩者都由 PrintingFishAction
和 FishColor
介面組成,但會將實作項目委派給這些介面。
介面委派功能十分強大,一般來說,只要您可能在其他語言中使用抽象類別,就應考慮如何使用這項功能。您可以使用組合插入行為,而不需大量子類別,每個子類別都以不同方式專門化。
資料類別與其他語言中的 struct
類似,主要用於保存資料,但資料類別物件仍是物件。Kotlin 資料類別物件可帶來一些額外優勢,例如用於列印和複製的公用程式。在這項工作中,您會建立簡單的資料類別,並瞭解 Kotlin 對資料類別提供的支援。
步驟 1:建立資料類別
- 在 example.myapp 套件下新增
decor
套件,用於存放新程式碼。在「Project」窗格中,對「example.myapp」按一下滑鼠右鍵,然後依序選取「File」>「New」>「Package」。 - 在套件中,建立名為
Decoration
的新類別。
package example.myapp.decor
class Decoration {
}
- 如要將
Decoration
設為資料類別,請在類別宣告前加上data
關鍵字。 - 新增名為
rocks
的String
屬性,為類別提供一些資料。
data class Decoration(val rocks: String) {
}
- 在檔案中,於類別外新增
makeDecorations()
函式,以建立及列印Decoration
的例項 (含"granite"
)。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- 新增
main()
函式來呼叫makeDecorations()
,然後執行程式。請注意,由於這是資料類別,因此會建立合理的輸出內容。
⇒ Decoration(rocks=granite)
- 在
makeDecorations()
中,再例項化兩個Decoration
物件 (皆為「slate」),然後列印這些物件。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- 在
makeDecorations()
中,新增輸出陳述式,輸出比較decoration1
與decoration2
的結果,以及比較decoration3
與decoration2
的結果。使用資料類別提供的 equals() 方法。
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- 執行程式碼。
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
步驟 2:使用解構
如要取得資料物件的屬性並指派給變數,可以一次指派一個屬性,如下所示。
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
您可以改為為每個屬性建立變數,然後將資料物件指派給變數群組。Kotlin 會將屬性值放入每個變數。
val (rock, wood, diver) = decoration
這稱為「解構」,是實用的簡寫方式。變數數量應與屬性數量相符,且變數會按照類別中宣告的順序指派。以下是完整的範例,您可以在 Decoration.kt 中試用。
// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}
fun makeDecorations() {
val d5 = Decoration2("crystal", "wood", "diver")
println(d5)
// Assign all properties to variables.
val (rock, wood, diver) = d5
println(rock)
println(wood)
println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver) crystal wood diver
如果不需要一或多個屬性,可以使用 _
(而非變數名稱) 略過這些屬性,如下列程式碼所示。
val (rock, _, diver) = d5
在本工作中,您將瞭解 Kotlin 中的一些專用類別,包括:
- 單例模式類別
- 列舉
- 密封類別
步驟 1:回想單例模式類別
回想一下先前 GoldColor
類別的範例。
object GoldColor : FishColor {
override val color = "gold"
}
由於每個 GoldColor
執行個體都會執行相同作業,因此會宣告為 object
,而非 class
,藉此設為單例模式。只能有一個執行個體。
步驟 2:建立列舉
Kotlin 也支援列舉,可讓您列舉項目並依名稱參照,與其他語言類似。在宣告前加上 enum
關鍵字,即可宣告列舉。基本列舉宣告只需要名稱清單,但您也可以定義與每個名稱相關聯的一或多個欄位。
- 在 Decoration.kt 中,試用列舉的範例。
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
列舉與單例模式有點類似,只能有一個,且列舉中的每個值只能有一個。舉例來說,只能有一個 Color.RED
、一個 Color.GREEN
和一個 Color.BLUE
。在本範例中,RGB 值會指派給 rgb
屬性,代表顏色元件。您也可以使用 ordinal
屬性取得列舉的序數值,並使用 name
屬性取得列舉的名稱。
- 請試試另一個列舉範例。
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}
⇒ EAST 2 90
步驟 3:建立密封類別
密封類別可以加入子類別,但只能在宣告類別的檔案內加入。如果您嘗試在其他檔案中將類別設為子類別,就會收到錯誤訊息。
由於類別和子類別位於同一個檔案中,Kotlin 會靜態瞭解所有子類別。也就是說,在編譯期間,編譯器會看到所有類別和子類別,並知道這些就是全部的類別,因此編譯器可以為您執行額外檢查。
- 在 AquariumFish.kt 中,試用密封類別的範例,並維持水生主題。
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}
Seal
類別無法在其他檔案中建立子類別。如要新增更多 Seal
類型,必須在同一個檔案中新增。因此密封類別是代表固定數量型別的安全方式。舉例來說,密封類別非常適合從網路 API 傳回成功或錯誤。
本課程涵蓋許多主題,雖然 Kotlin 的許多概念與其他物件導向程式設計語言類似,但 Kotlin 新增了一些功能,可讓程式碼保持簡潔易讀。
類別和建構函式
- 使用
class
在 Kotlin 中定義類別。 - Kotlin 會自動為屬性建立 setter 和 getter。
- 直接在類別定義中定義主要建構函式。例如:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- 如果主要建構函式需要額外程式碼,請在一或多個
init
區塊中撰寫。 - 類別可以使用
constructor
定義一或多個次要建構函式,但 Kotlin 樣式是改用工廠函式。
瀏覽權限修飾符和子類別
- Kotlin 中的所有類別和函式預設為
public
,但您可以使用修飾符將瀏覽權限變更為internal
、private
或protected
。 - 如要建立子類別,父項類別必須標示為
open
。 - 如要在子類別中覆寫方法和屬性,必須在父項類別中將方法和屬性標示為
open
。 - 密封類別只能在定義所在的檔案中建立子類別。在宣告前加上
sealed
,即可建立密封類別。
資料類別、單例模式和列舉
- 在宣告前加上
data
,即可建立資料類別。 - 解構是將
data
物件的屬性指派給個別變數的簡寫。 - 請使用
object
而非class
,建立單例類別。 - 使用
enum class
定義列舉。
抽象類別、介面和委派
- 抽象類別和介面是兩種在類別之間共用常見行為的方式。
- 抽象類別會定義屬性和行為,但實作內容則留給子類別。
- 介面會定義行為,並可能為部分或所有行為提供預設實作內容。
- 使用介面組合類別時,類別所含的類別例項會擴充類別的功能。
- 介面委派會使用組合,但也會將實作項目委派給介面類別。
- 組合是透過介面委派將功能新增至類別的強大方式。一般來說,組合是較好的做法,但有些問題更適合從抽象類別繼承。
Kotlin 說明文件
如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org。
Kotlin 教學課程
https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。
Udacity 課程
如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。
IntelliJ IDEA
IntelliJ IDEA 的說明文件位於 JetBrains 網站。
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
類別具有特殊方法,可做為建立該類別物件的藍圖。這個方法稱為?
▢ 建構工具
▢ 執行個體化工具
▢ 建構函式
▢ 藍圖
第 2 題
下列有關介面和抽象類別的敘述何者不正確?
▢ 抽象類別可以有建構函式。
▢ 介面不得有建構函式。
▢ 介面和抽象類別可以直接例項化。
▢ 抽象屬性必須由抽象類別的子類別實作。
第 3 題
下列何者「不是」Kotlin 屬性、方法等的瀏覽權限修飾符?
▢ internal
▢ nosubclass
▢ protected
▢ private
第 4 題
請考慮這個資料類別:data class Fish(val name: String, val species:String, val colors:String)
以下哪一個程式碼「不」正確,無法建立及解構 Fish
物件?
▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")
▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")
▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")
▢ val (name4, species4, colors4) = Fish("Harry", "halibut")
第 5 題
假設您經營動物園,園內有許多動物需要照料。下列何者「不」屬於照護實施範圍?
▢ interface
動物吃的不同食物類型。
▢ abstract Caretaker
類別,可從中建立不同類型的照護者。
▢ interface
為動物提供乾淨的水。
▢ 餵食時間表項目的 data
類別。
繼續下一個課程:
如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。」