初見類別

我們前面了解了很多Kotlin內建的型態,而在程式碼的背後這些型態都是用類別來定義的。類別就像是一張履歷表,紀錄著這個型態有什麼屬性與函數,當我們建立物件的時候,編譯器就會去查看這個我們即將要建立的型態有什麼東西,並把物件建立出來

自己定義類別

在我們自己建立類別的時候,我們可以先簡單想想看,假如我要建立“狗”的類別,我需要什麼東西?例如:我養了一隻狗,他的名字叫做小黑、今年年齡4歲、體重是17kg、品種是台灣土狗,他會東西並且會跟我丟接球的遊戲......

有了這些資訊我們可以整理出下面的類別圖:

類別名稱
Dog

屬性

name、age、weight、breed

函數

bark()、eat()、play()

接下來,我們就來看看要怎麼定義類別

定義狗狗的類別

class Dog(val name: String, var age: Int, var weight: Int, val breed: String) {
    fun bark() {
        println("Woo ou ou ou o ou ooof!")
    }

    fun eat() {
        println("$name is eating.")
    }

    fun play() {
        if (age < 1) {
            println("Running everywhere")
        } else {
            println("Catch ball and take back.")
        }
    }
}

在這裡我們使用了class關鍵字來定義類別並且把這個類別命名為Dog,在後面的“()”裡面放了這個類別的屬性,並且我們在類別的區塊裡面宣告了三個函數

類別與物件

現在我們成功的定義好了Dog類別,可是類別就像是一張描述了“狗狗是什麼”的維基百科頁面,我們想要跟真正的狗互動而不是看著維基百科過過乾癮。所以我們需要一個“實體”才能在程式裡面跟他互動,這個實體我們稱為“物件”,那我們要怎麼把狗狗實體化呢?

val dog = Dog("小黑", 4, 17, "台灣土狗")

我們前面在定義狗狗的時候定義了4個屬性,所以我們在實體化的時候也要依序傳入4個引數給屬性使用,當然如同函數一樣我們也可以使用具名呼叫

val dog = Dog(
        name = "小黑", 
        age = 4, 
        weight = 17, 
        breed = "台灣土狗"
    )

上面在建立實體的時候呼叫的這個看起來很像函數的東西,就是我們在定義類別的這一段:

(val name: String, var age: Int, var weight: Int, val breed: String)

這個部分被我們稱為建構式,建構式的功用就是實體化物件,並且初始化屬性。建立好了物件之後我們就可以在程式裡用“.”來使用狗狗的屬性跟函數

println(dog.name)
dog.bark()
dog.bark()
dog.bark()
dog.bark()
dog.bark()
// 這隻狗怎麼這麼吵...

類別的屬性

目前我們在Dog的建構式裡面可以宣告物件的屬性,這是一種簡化式的寫法,實際上比較複雜一點的寫法是這樣:

class Dog(nameParam: String, ageParam: Int, weightParam: Int, breedParam: String) {
    val name = nameParam
    var age = ageParam
    var weight = weightParam
    val breed = breedParam
    // ......
}

這邊建構式裡的參數都沒有val或var,代表他們已經不是物件的屬性,而是一般的參數,而我們在定義建構式的時候可以一般的參數跟類別屬性共存。

屬性的初始化

從上面的例子我們可以看到,宣告在類別內部的屬性我們需要給他初始值,而宣告在建構式裡面的屬性可以不需要,因為在建構式裡面的屬性會在呼叫建構式的時候給予初始值。接下來我們來看看屬性還可以怎麼初始化:

class Dog(val name: String, var age: Int, var weight: Int, breedParam: String) {
    val activities = listOf("Walk", "Run", "Sleep")
    val breed = breedParam.uppercase()
    // ......
}

使用初始化區塊

如果我們需要在初始化的時候做一些複雜的運算,或是想要做一些其他事情的話,可以使用初始化區塊:

class Dog(val name: String, var age: Int, var weight: Int, breedParam: String) {
    val activities = listOf("Walk", "Run", "Sleep")
    val breed = breedParam.uppercase()
    val bornYear: Int
    
    init {
        val currentYear = Calendar.getInstance()[Calendar.YEAR]
        bornYear = currentYear - age + 1
        println("成功創建一隻狗狗,名字叫$name")
    }
    // ......
}

也可以有多個初始化區塊,而類別在初始化的時候會依序由上往下執行

import java.util.Calendar

class Dog(val name: String, var age: Int, var weight: Int, breedParam: String) {
    val activities = listOf("Walk", "Run", "Sleep")
    val bornYear: Int

    init {
        println("成功創建一隻狗狗,名字叫$name")
        val currentYear = Calendar.getInstance()[Calendar.YEAR]
        bornYear = currentYear - age + 1
        // 這個初始化區塊在breed創建之前執行,所以沒辦法使用breed屬性
    }
    val breed = breedParam.uppercase()
    
    init {
        println("狗狗的品種是$breed")
    }
    // ......
}

宣告的屬性必須初始化

在Kotlin裡面一個變數如果沒有被初始化的話,編譯器是不會賦予它初始值的。所以我們之前在函數裡面宣告的變數,即便一開始沒有給定初始值, 在第一次使用之前也必須賦予初始值才能使用。而類別中定義的屬性,是在物件被初始化成功後就可以開始被使用的。為了防止我們忘記初始化屬性,所以編譯器強制我們一定要初始化每一個屬性才可以正常執行我們的程式

var 延遲初始化

雖然編譯器會強制我們初始化每一個屬性,但是我們總是會遇到在宣告類別的時候真的沒辦法給定初始值的情況,這個時候就可以使用“lateinit”關鍵字標記屬性,讓我們可以暫時不初始化他。不過就算這麼做,我們還是得要在使用之前把屬性初始化,不然就會報錯。

lateinit var x: String

雖然有了lateinit讓我們得以延遲屬性的初始化,可是lateinit並不是所有的情況下都適用。比如:lateinit只能使用在宣告為var的屬性上,而且不能使用在這些型態上:Byte、Short、Int、Long、Double、Float、Char、Boolean。那val的變數有沒有方法可以延遲初始化呢?

val 惰性初始化

有時候我們在初始化屬性的時候會執行一些相對來說比較耗費資源的任務:讀取檔案、龐大的計算,或是單純需要等待其他屬性先初始化,這個時候我們就可以使用惰性初始化。惰性初始化就是我們先定義好這個屬性的數值要怎麼樣運算出來(lambda),但是我們在初始化的時候先不執行,等到第一次呼叫這個屬性的時候才會執行並設定

lateinit var x: String
val y by lazy { x.uppercase() }

讓屬性變得可控

現在我們已經學會怎麼取得、設定屬性的值了!比如說像這樣:

println(dog.name) // 印出狗狗的名字
dog.weight = 20 // 狗狗變重了
dog.weight = -1 // Oh my God!!!!

我們發現這樣讓其他人隨意的存取屬性是很可怕的一件事情,為了防止這種事情發生,我們需要學習一些其他的手段

自定義getter與setter

我們可以實作getter跟setter可以讓屬性變得可以受控制。getter唯一的用途就是傳出我們想讓人看到的屬性值,setter會接收一個引數並使用這個引數去做一些事情。

getter

我們先來看看要怎麼實作getter,比方說我今天也需要一個屬性,是以磅為單位的體重,我們自然不會想要磅數跟公斤數對不起來,所以我們可以這樣子做:

class Dog(val name: String, var age: Int, var weight: Int, breedParam: String) {
    val weightInPounds: Double
        get() {
            return weight * 2.2
        }
    // ......
}

我們加入了一個型態是Double的屬性weightInPounds,並且實作了getter。getter是一個沒有參數的函數,回傳值跟屬性相同。跟函數一樣,只有一行的getter可以簡化寫成

val weightInPounds: Double
        get() = weight * 2.2

這樣每當我們呼叫weightInPounds的時候,就會呼叫getter得到運算後的結果。

setter

再來我們需要讓體重變得可被控制,這樣我們要從setter下手:

class Dog(val name: String, var age: Int, weightParam: Int, breedParam: String) {
    var weight = weightParam
        set(value) {
            if (value > 0) field = value
        }
    // ......
}

setter是一個只有一個參數的函數,這個參數我們習慣取名為value,他的型態是屬性的型態。實作了setter後我們就能成功防止有其他人把體重設成負數了。

‘setter裡面有一個field那是什麼?’

“field是setter裡面特有的東西,代表屬性的幕後屬性。在setter裡面我們會使用field來設定屬性的值,而不是直接設定屬性。因為當我們設定屬性的時候會再次呼叫setter,導致setter一直無限的被重覆呼叫而造成崩潰”

類別的函數

基本上類別函數跟一般的函數沒什麼特別不同的地方,唯一的不同點在於類別函數可以取用設定自己類別的屬性

可見性與封裝

在Kotlin裡面有四個可見性修飾符,我們來認識認識他們:

修飾符

public(預設)

外部可見,代表任何地方都可以取用

private

內部可見,只有在類別內部可以取用

protected

只有在類別內部以及該類別的子類別內部可見(將會在繼承的時候介紹)

internal

在同一個模組裡面可見

到目前為止,我們在宣告類別的時候都沒有特別對於可見性做處理,導致我們在任何地方都可以隨意的取用Dog的任何東西,這樣其實是很危險的!比如說我們今天想要實作狗狗的消化系統,可是我們並沒有特別設定可見性,這樣就會導致消化系統可以在任何地方可見,並且使用,這樣子實在是太獵奇了!!!為了防止這樣的事情發生我們必須要把一些不需要讓外部可見、取用的部分設定成不可見,這樣的行為叫做“封裝”,也是物件導向程式設計裡面一個很重要的概念。

Last updated