這個程式碼研究室是「程式設計人員的 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 新手上路課程:歡迎參加本課程。」