面向對象(三)

擴展

Kotlin 的擴展是一個很獨特的功能, Java本身并不支持擴展, Kotlin 為了讓擴展能在JVM平臺上運行,必須做一些獨特的處理 。 本節將會詳細介紹這些處理 。
Kotlin 支持擴展方法和擴展屬性 。

擴展方法

擴展方法的語法很簡單,其實就是定義一個函數,只是函數名不要寫成簡單的函數,而是要在函數名前增加被擴展的類(或接口)名和點號(.) 。 例如如下程序

open class Raw{
    fun test(){
        println("test()方法")
    }
}

class RawSub :Raw(){
    fun sub(){
        println("--sub()方法--")
    }
}

//定義一個函數 ,函數名為“被擴展類.方法”
fun Raw.info(){
    println("--擴展的info()方法--")
}

fun main(args: Array<String>) {
    var r =Raw()
    r.test()
    //調用 Raw 對象擴展的方法
    r.info()
    //創建 Raw 類的子類的實例
    var rsub = RawSub()
    rsub.sub()
    rsub.test()
    //Raw 的子類的對象也可調用擴展的方法
    rsub.info()
}

上面程序定義了 一個 Raw 類,該 Raw 類使用了 open 修飾符,這是為了示范給 Raw 類派生子類 。 如果只是對 Raw 類進行擴展,那么 Kotlin 并不需要使用 open 修飾該類 。
Raw 類擴展了 info()方法之后,就像為 Raw 類增加了 info()方法一樣,所有的 Raw 對象都可調用 info()方法。不僅如此,Raw類的子類的實例也可調用 info方法。就像真的為Raw類增加了一個方法一樣。擴展方法中的 this 與成員方法中的 this 一樣 ,也代表調用該方法的對象 。

擴展也可以為系統的類增加方法,下面的程序將為系統的List集合拓展一個shuffle()方法,該方法用于對集合元素進行隨機排列 。

import java.util.*

//該方法的實現思路是:先生成 List 集合所有索引的隨機排列
// 然后根據隨機排列的索引去 List 集合中取元素
fun List<Int>.shuffle(): List<Int> {
    val size = this.size
    //下面的 indexArr 用于保存 List 集合的索引的隨機排列
    var indexArr = Array<Int>(size, { 0 })
    var result: MutableList<Int> = mutableListOf()
    //創建隨機對象
    val rand = Random()
    var i = 0
    outer@ while (i < size) {
        //生成隨機數
        var r = rand.nextInt(size)
        for (j in 0 until i) {
            //如果 r 和前面已生成的任意數字相等,則該隨機數不可用,需要重新生成
            if(r == indexArr[j]){
                continue@outer
            }
        }

        //如果上面的循環結束了都沒有執行 continue,則說明該 r 是一個不重復的隨機數
        //將隨機數 r 存入 indexArr 數組中
        indexArr[i] = r
        //根據隨機的索引讀取 List 集合元素,并將元素添加到 result 集合中
        result.add(this[r])
        i++
    }

    return result.toList()
}

fun main(args: Array<String>) {
    var nums = listOf<Int>(20,19,23,1,23,64)
    //調用程序為 List 擴展的 shuffle ()方法
    println(nums.shuffle())
    println(nums.shuffle())
}

上面程序擴展List時指定了泛型參數為Int,因此該擴展只對List<Int>有效。

實際上 Kotlin 完全支持直接進行泛型類擴展,只不過此時需要在函數上使用泛型,因此要使用泛型函數的語法。

//該方法的實現思路是:先生成 List 集合所有索引的隨機排列
// 然后根據隨機排列的索引去 List 集合中取元素
fun <T> List<T>.shuffle(): List<T> {
    val size = this.size
    //下面的 indexArr 用于保存 List 集合的索引的隨機排列
    var indexArr = Array<Int>(size, { 0 })
    var result: MutableList<T> = mutableListOf()
    //創建隨機對象
    val rand = Random()
    var i = 0
    outer@ while (i < size) {
        //生成隨機數
        var r = rand.nextInt(size)
        for (j in 0 until i) {
            //如果 r 和前面已生成的任意數字相等,則該隨機數不可用,需要重新生成
            if(r == indexArr[j]){
                continue@outer
            }
        }

        //如果上面的循環結束了都沒有執行 continue,則說明該 r 是一個不重復的隨機數
        //將隨機數 r 存入 indexArr 數組中
        indexArr[i] = r
        //根據隨機的索引讀取 List 集合元素,并將元素添加到 result 集合中
        result.add(this[r])
        i++
    }

    return result.toList()
}

fun main(args: Array<String>) {
    var strList = listOf<String>("ss","111","nnn","dd","vv")
    //調用程序為 List 擴展的 shuffle ()方法
    println(strList.shuffle())
    println(strList.shuffle())
}

擴展的實現機制

我們知道, Java是一門靜態語言。一個類被定義完成之后,程序無法動態地為該類增加、 刪除成員( field、方法等),除非開發者重新編輯該類的源代碼,并重新編譯該類。
但現在 Kotlin 的擴展卻好像可以動態地為一個類增加新的方法,而且不需要重新修改該類的源代碼,這真是太神奇了!那 Kotlin 擴展的實際情況是怎樣的呢?難道 Kotlin 可以突破JVM的限制?
實際上, Kotlin的擴展并沒有真正地修改所擴展的類,被擴展的類還是原來的類,沒有任何改變。 Kotlin擴展的本質就是定義了一個函數,當程序用對象調用擴展方法時, Kotlin在編譯時會執行靜態解析一一就是根據調用對象、方法名找到擴展函數,轉換為函數調用。例如:

strList.shuffle()

Kotlin 在編譯時這行代碼按如下步驟執行:

  1. 檢查 strList 的類型,發現其類型是 List<String>。
  2. 檢查 List<String>類本身是否定義了 shuffle()方法,如果該類本身包含該方法,則 Kotlin無須進行處理,直接編譯即可。
  3. 如果 List<String>類本身不包含 shuffle()方法,則 Kotlin會查找程序是否為 List<String> 擴展了 shuffle()方法一一也就是查找系統中是否包含了名為 List<String>.shuffile()的函數(或泛型函數)定義,如果找到該函數,則 Kotlin 編譯器會執行靜態解析,它會將上面代碼替換成執行 List<String>.shuffile()函數。
  4. 如果 List<String>不包含 shuffle()方法,也找不到名為 List<String>.shuffile()的函數(或泛型函數)定義,編譯器將報錯。

由此可見, Kotlin 的擴展并沒有真正改變被擴展的類, Kotlin 只是耍了一個小“花招”,當 Kotlin 程序調用擴展方法時, Kotlin 編譯器會將這行代碼靜態解析為調用函數,這樣 JVM 就可接受了。這意味著調用擴展方法是由其所在表達式的編譯時類型決定的,而不是由它所在表達式的運行時類型決定的。

//定義一個 Base 類
open class Base

//定義 Sub 類繼承 Base 類
class Sub : Base()

//為 Base 類擴展 foo 方法
fun Base.foo(){
    println("Base擴展的 foo()方法")
}
//為 Sub 類擴展 foo 方法
fun Sub.foo() = println("Sub擴展的foo()方法")

//定義一個函數
fun invokeFoo(b:Base){
    //調用 Base 對象的 foo ()方法
    b.foo()
}

fun main(args: Array<String>) {
    //傳入的是 Sub對象
    //輸出Base擴展的 foo()方法
    invokeFoo(Sub())

    val bb: Base = Sub()
    //輸出Base擴展的 foo()方法
    bb.foo()
}

上面程序中定義了 一個 Base 類及其子類 Sub,接下來程序為 Base 和 Sub 分別擴展了 foo() 方法。
對于有繼承關系的 Base和 Sub兩個類,它們都包含了具有相同簽名的 foo()方法,如果 foo() 方法是成員方法,程序就必須用 override 聲明子類方法重寫父類方法;但由于此處采用的是擴展, 因此 Kotlin 不需要聲明為方法重寫。
上面程序中定義了 一個 invokeFoo(Base)函數,該函數的形參類型是Base,但調用該函數時傳入一個 Sub對象,那么此處到底是調用 Base類擴展的 foo()方法, 還是調用 Sub類擴展的 foo()方法呢?
如果foo()方法是Base、 Sub所包含的成員方法(方法重寫),那么一定是由JVM動態解析為調用運行時類型( Sub 對象)的方法。
但此處的 foo()方法是 Base、 Sub 所包含的擴展方法,對于擴展方法由于 Kotlin 編譯器執行靜態解析,在編譯階段 Kotlin編譯器只知道 invokeFoo()方法的形參是 Base類型,因此 Kotlin 編譯器會將其替換為調用 Base 的 foo()方法。

總結起來一句話 : 成員方法執行動態解析(由運行時類型決定);擴展方法執行靜態解析(由編譯時類型決定)。

此外,前面介紹了 Kotlin 編譯時解析一個成員方法和擴展方法的步驟,由此可知成員方法的優先級高于擴展方法。這意味著 ,如果一個類包含了具有相同簽名的成員方法和擴展方法,當程序調用這個方法時,系統總是會執行成員方法,而不會執行擴展方法。

為可空類型擴展方法

Kotlin 還允許為可空類型(帶“?”后綴的類型)擴展方法。由于可空類型允許接受null 值,這樣使得 null值也可調用該擴展方法。從另一方面來看 , 由于會導致null值調用該擴展方法,因此程序需要在擴展方法中處理null值的情形。例如如下程序。

//為可空類型擴展 equals 方法
fun Any?.equals(other: Any?): Boolean {
    if (this == null) {
        return if (other == null) true else false
    }

    return this.equals(other)
}

fun main(args: Array<String>) {
    var a = null
    println(a.equals(null)) //輸出 true
    println(a.equals("sss"))  //輸出 false
}

擴展屬性

Kotlin也允許擴展屬性,但由于 Kotlin 的擴展并不能真正修改目標類,因此 Kotlin擴展的屬性其實是通過添加 getter、 setter 方法實現的,沒有幕后字段。簡單來說,擴展的屬性只能是計算屬性 !
由于 Kotlin 的擴展屬性只能是計算屬性,因此對擴展屬性有如下三個限制。

  • 擴展屬性不能有初始值(沒有存儲屬性值的幕后字段) 。
  • 不能用 field關鍵字顯式訪問幕后字段。
  • 擴展只讀屬性必須提供 getter方法;擴展讀寫屬性必須提供 getter、 setter方法。 如下程序示范了為 Kotlin類添加擴展屬性。
class User(var first: String, var last: String)

//為 User 擴展讀寫屬性
var User.fullName: String
    get() = "${last}.${first}"
    set(value) {
        println("執行擴展屬性 fullName 的 setter 方法")
        //value 字符串中不包含.或包含幾個 .都不行
        if ("." !in value || value.indexOf(".") != value.lastIndexOf(".")) {
            println("您輸入的 fullName 不合法")
        } else {
            var tokens = value.split(".")
            first = tokens[1]
            last = tokens[0]
        }
    }

fun main(args: Array<String>) {
    var user=User("悟空","孫")
    println(user.fullName)
    user.fullName="八戒.豬"
    println(user.last)
    println(user.first)
}

此外,由于擴展屬性的本質就是 getter、 setter方法,因此也可用泛型函數的形式來定義擴展屬性。例如, Kotlin為 List擴展的 lastIndex屬性的代碼如下:

val <T> List<T>.lastindex: Int
    get() = size - 1

以成員方式定義擴展

前面見到的擴展,都是以頂層函數的形式(放在包空間下)進行定義的,因此這些擴展都可直接使用(如果擴展位于不同的包中,當然也需要導包)。 Kotlin 還支持以類成員的方式定義擴展一一就像為類定義方法、屬性那樣定義擴展。
對于以類成員方式定義的擴展,一方面它屬于被擴展的類,因此在擴展方法(屬性〉中可直接調用被擴展類的成員(省略 this 前綴): 另一方面它又位于定義它所在類的類體中 , 因此在擴展方法(屬性)中又可直接調用它所在類的成員(省略 this 前綴)。

class A {
    fun bar() = println("A的bar方法")
}

class B {
    fun baz() = println("B的 baz 方法")

    //以成員方式為A擴展 foo()方法
    fun A.foo() {
        //在該方法內既可調用類 A 的成員,也可調用類 B 的成員
        // A對象為隱式調用者
        bar()
        //B對象為隱式調用者
        baz()
    }

    fun test(target: A) {
        //調用 A 對象的成員方法
        target.bar()
        //調用 A 對象的擴展方法
        target.foo()
    }
}

fun main(args: Array<String>) {
    var b = B()
    b.test(A())
}

上面程序在類 B 中為類 A 擴展了一個 foo()方法。由于該foo()方法一方面屬于類 A,另一 方面又定義在類 B 中,因此在 foo()方法內既可直接調用類 A 的成員,也可直接調用類 B 的成員,且都不需要使用 this前綴。

這樣又會產生一個新的問題。如果被擴展類和擴展定義所在的類包含了同名的方法,此時就會導致:程序在擴展方法中調用兩個類都包含的方法時,系統總是優先調用被擴展類的方法。為了讓系統調用擴展定義所在類的方法,必須使用帶標簽的this進行限定。

class Tiget {
    fun foo() {
        println("Tiger 類的foo()方法")
    }
}

class Bear {
    fun foo() {
        println("Bear 類的foo()方法")
    }

    //以成員方式為 Tiger 類擴展 test ()方法
    fun Tiget.test() {
        foo()
        //使用帶標簽的 this 指定調用 Bear 的 foo ()方法
        this@Bear.foo()
    }

    fun info(tiget: Tiget) {
        tiget.test()
    }

}

fun main(args: Array<String>) {
    val b = Bear()
    b.info(Tiget())
}

上面程序在 Bear類中為 Tiger類擴展了一個 test()方法,且 Bear類和 Tiger類都包含 foo() 方法,因此如果程序在 test()擴展方法中調用 foo()方法,系統總是優先調用 Tiger類的foo()方 法。為了在 test()擴展方法中調用 Bear定義的 foo()方法,就需要使用 this@Bear前綴,如上面程序所示。
Kotlin 的 this 比 Java 的 this 更強大, Kotlin 的 this 支持用“@類名”形式,這種形式限制了該 this 代表哪個類的對象。

帶接收者的匿名函數

Kotlin還支持為類擴展匿名函數,在這種情況下,該擴展函數所屬的類也是該函數的接收者。因此,這種匿名函數也被稱為“帶接收者的匿名函數”。
與普通擴展方法不同的是 :去掉被擴展類的類名和點(.)之后的函數名即可,其他部分并沒有太大的區別。與普通擴展方法相似的是,帶接收者的匿名函數(相當于擴展匿名函數)也允許在函數體內訪問接收者對象的成員。

//定義一個帶接收者的匿名函數
val factorial = fun Int.(): Int {
    //該匿名函數的接收者是 Int 對象
    //因此在該匿名函數中, this代表調用該匿名函數的Int對象
    if (this < 0) {
        return -1
    } else if (this == 1) {
        return 1
    } else {
        var result = 1
        for (i in 1..this) {
            result *= i
        }

        return  result
    }
}

fun main(args: Array<String>) {

    println(6.factorial())
}

上面程序中定義了一個帶接收者的匿名函數,相當于程序為 Int 擴展了一個匿名函數。如果寫成 fun Int.abc(): Int,就表示為 Int類擴展了 abc()方法;但是 fun Int.(): Int,此時 Int后什么也沒有,也就是沒有指定函數名,因此它是一個匿名函數。

由于上面程序最后將帶接收者的匿名函數賦值給了 factorial變量,因此可通過 Int對象來調用 factorial()函數。

與普通函數相似的是,帶接收者的匿名函數也有自身的類型,即帶接收者的函數類型。例如,上面 factorial變量的類型為:

Int.()->Int

該類型就是在普通函數類型的前面添加了一個接收者類型進行限定。

如果接收者類型可通過上下文推斷出來,那么 Kotlin允許使用 Lambda表達式作為帶接收者的匿名函數 。 例如如下程序 。

class Html {
    fun body() {
        println(" <body></body>")
    }

    fun head() {
        println("  <head></head>")
    }
}

//定義一個類型為 Html. ()->Unit 的形參(帶接收者的匿名函數)
//這樣在函數中 Html 對象就增加了 一個 init 方法
fun html(init: Html.() -> Unit) {
    println("<html>")
    val html = Html() //創建接收者對象
    html.init() //使用接收者調用 init 引用匿名函數(即傳入的參數)
    println("<html>")
}

fun main(args: Array<String>) {
    //調用 html 函數,需要傳入 Html.()-> Unit 類型的參數
    //此時系統可推斷出接收者的類型,故可用 Lambda 表達式代替匿名函數
    html {
        //Lambda 表達式中的 this 就是該方法的調用者
        head()
        body()
    }
}

上面程序先定義了 一個Html 類,并在該類中定義了 body()和 head()兩個方法,這個類沒有任何特別的地方 。
接下來程序定義了一個 html()函數,該函數的形參是 HTML.()->Unit 類型,即帶接收者的函數類型 。 html()函數的形參名為init,這意味著當程序調用html()函數時傳入的匿名函數(或 Lambda 表達式)會傳給該 init 參數,這樣在該 html()函數內, HTML 對象就被動態擴展了一個 init()方法,而且該方法的執行體是動態傳入的 。
在 main()函數中調用 html()函數,調用語法看上去有點奇怪,這一點在以前有介紹:如果調用函數只有一個 Lambda 表達式參數,則可以省略調用函數的圓括號,將Lambda 表達式放在函數之外即可。也就是說,main()函數中代碼的完整形式如下:

    html({
        //Lambda 表達式中的 this 就是該方法的調用者
        head()
        body()
    })

何時使用擴展

擴展無非就是為類增加方法或屬性, 為什么不直接在類中定義這些方法和屬性,還要通過擴展來定義呢?這不是多此一舉嗎?
多現代編程語言都已支持擴展,由此可見擴展的魅力 。
擴展的作用主要有如下兩個方面 。

擴展可動態地為已有的類添加方法或屬性 。
擴展能以更好的形式組織一些工具方法 。

關于上面第一點,有時候我們要為一些已有框架或庫的類增加額外的功能,如果使用 Java,則只能通過派生子類來實現,但一方面派生子類有一些限制,另一方面派生子類的性能開銷也比較大。而擴展的出現很好地解決了這個問題 。
擴展是一種非常靈活的動態機制,它既不需要使用繼承,也不需要使用類似于裝飾者的任何設計模式,即可為現有的類增加功能,因此使用非常方便。
關于上面第二點,我們知道Java 系統提供了 Arrays、 Collections、 Files 等各種類,還有第三方庫提供的大量的 StringUtils、 BeanUtils等類,這些類的作用非常明確:工具類,包含了操作特定類型的工具方法 。 比如 Arrays 包含了大量操作數組的工具方法, Collections 包含 了大量操作集合的工具方法 。 例如要使用 Collections對 List集合元素排序,則需要使用如下代碼:

Collections.sort(list)

上面這行代碼平白無故地多出一個 Collections 工具類,而且這也不符合面向對象的代碼風格(面向對象的代碼風格是:豬八戒.吃(西瓜)),這里多出來的 Collections類真讓人尷尬。
其實希望使用如下更簡潔的代碼:

list. sort ()

此時就需要讓 List 集合本身具有 Collections 類提供的工具方法,通過擴展即可為 List 集合增加這些工具方法。

final和 open修飾符

final 關鍵字可用于修飾類、屬性和方法,表示它修飾的類、屬性和方法不可改變 。
Kotlin 有一個非常特別的設計:它會為非抽象類自動添加 final 修飾符,也會為非抽象方法、非抽象屬性等無須重寫的成員自動添加final修飾符。如果開發者希望取消Kotlin自動添加final修飾符,則可使用 open 修飾符, open 修飾符與final 修飾符是反義詞。
此外,Kotlin 與 Java 的一個重大區別是: final 修飾符不能修飾局部變量,因此open 自然也不能修飾局部變量 。

可執行“宏替換”的常量

java使用 final 修飾“宏變量”,該“宏變量”在編譯階段就會被替換掉 。
Kotlin提供了const用來修飾可執行“宏替換”的常量,這種常量也被稱為“編譯時”常量 ,因為它在編譯階段就會被替換掉 。
“宏替換”的常量除使用 const修飾之外,還必須滿足如下條件。

  • 位于頂層或者是對象表達式的成員。
  • 初始值為基本類型值 (Java 的 8 種基本類型)或字符串字面值。
  • 沒有自定義的 getter方法。
//定義支持 “宏替換”的常量
const val MAX_VALUE =100

fun main(args: Array<String>) {
    println(MAX_VALUE)
}

上面程序中使用 const定義了一個支持“宏替換”的常量,并在定義該常量 時指定初始值為 100。對于這個程序來說,常量 MAX AGE 其實根本不存在,當程序執行 println(MAX_AGE)代碼時,實際替換為執行 println(l00),而且這個替換在編譯階段就完成 了,因此程序在運行階段完全沒有 MAX_AGE 常量。
此外,如果被賦值的表達式只是基本的算術表達式或進行字符串連接運算,沒有訪問普通變量、常量,調用方法,那么 Kotlin編譯器同樣會將這種 const常量當成“宏變量”處理。

final 屬性

final屬性表明該屬性不能被重寫,而且如果程序對屬性不使用任何修飾符, Kotlin會自動為該屬性添加final修飾。

final 方法

使用 final 修飾的方法不可被重寫。與屬性設計類似的是,如果程序不為方法添加任何修飾符, Kotlin 會自動為該方法添加 final 修飾。

final 類

使用 final 修飾的類不可以有子類,與方法、屬性的設計相同:如果一個類沒有顯式使用 open修飾符修飾,那么 Kotlin會自動為該類添加 final修飾。

不可變類

不可變( immutable)類的意思是創建該類的實例后,該實例的屬性值是不可改變的 。
如果需要創建自定義的不可變類,可遵守如下規則。

  • 提供帶參數的構造器,用于根據傳入的參數來初始化類中的屬性。
  • 定義使用 final修飾的只讀屬性,避免程序通過 setter方法改變該屬性值。

如果有必要,則重寫Any類的 hashCode()和 equals()方法。 equals()方法將關鍵屬性作為兩個對象是否相等的標準。除此之外,還應該保證兩個用 equals()方法判斷為相等的對象的hashCode()也相等 。

例如,String這個類就做得很好,它就是將String對象中的字符序列作為相等的標準,其hashCode()方法也是根據字符序列計算得到的。
下面定義一個不可變的 Address 類,程序把 Address 類的 detail 和 postCode 屬性都定義成只讀屬性,并使用 final修飾這兩個屬性,不允許其他方法修改這兩個屬性的值。

//定義可初始化兩個屬性的構造器
class Adress(val detail: String, val postCode: String) {
    override fun equals(other: Any?): Boolean {
        if (this == other) {
            return true
        }

        if (other == null) {
            return false
        }

        if (other.javaClass == Adress::class) {
            var ad = other as Adress
            //當 detail 和 postcode 相等時,可認為兩個 Address 對象相等
            return this.detail.equals(ad.detail) && this.postCode.equals(ad.postCode)
        }
        return false
    }

    override fun hashCode(): Int {
        return detail.hashCode() + postCode.hashCode()*31
    }
}

對于上面的 Address類,當程序創建了 Address對象后,同樣無法修改該 Address對象的detail 和 postCode 屬性 。
與不可變類對應的是可變類,其含義是該類的對象的屬性值是可變的。大部分時候所創建的類都是可變類,只要我們定義了任何讀寫屬性,該類就是可變類。
與可變類相比,不可變類的實例在整個生命周期中永遠處于初始化狀態,它的屬性值不可改變。因此,對不可變類的對象的控制將更加簡單。
有一個問題需要說明 : 當創建不可變類時,如果它包含的成員屬性的類型是可變的,那么其對象的屬性值依然是可改變的,這個不可變類其實是失敗的。

下面程序試圖定義一個不可變的Person類,但因為Person 類包含一個可變類型的屬性,所以導致Person類也變成了可變類。

class Name(var firstName: String = "", var lastName: String = "") {

}

class Person(val name: Name){

}

fun main(args: Array<String>) {
    var name = Name("悟空","孫")
    var p = Person(name)
    //輸出悟空
    println(p.name.firstName)
    //改變 Person 對象的 name 的 firstName 值
    p.name.firstName="八戒"
    //Person 對象的 name 的 firstName 值被改為 ”八戒 ”
    println(p.name.firstName)
}

上面程序中代碼修改了 Name 對象(可變類的實例)的firstName 的值,但由于 Person 類的 name屬性引用了該Name對象,就會導致Person對象的 name的 firstName的值會被改變,這就破壞了設計 Person類的初衷。
為了保持 Person對象的不可變性,必須保護好 Person對象的引用類型的屬性: name,讓程序無法訪問到 Person對象的 name 屬性的幕后變量,也就無法利用 name 屬性的可變性來改變Person對象了。為此將 Person 類改為如下形式:

class Name(var firstName: String = "", var lastName: String = "") {

}

class Person{
    val name:Name
    //返回一個新的對象,該對象的 firstName 和 lastName
    //與該 Person 對象里的幕后字段的 firstNarne 和 lastName 相同
    get() = Name(field.firstName,field.lastName)

    constructor(name:Name){
        //設置 name 屬性值為新創建的 Name 對象,該對象的 firstName 和 lastName
        //與傳入的 name 參數的 firstName 和 lastName 相同
        this.name = Name(name.firstName,name.lastName)
    }
}

注意閱讀上面代碼, Person 類改寫了設置 name 屬性的方法,也改寫了name屬性的 getter方法。當程序向 Person構造器里傳入一個 Name對象時,該構造器創建 Person 對象時并不是直接利用已有的 Name 對象(利用已有的 Name 對象有風險,因為這個已有的 Name 對象是可變的,如果程序改變了這個 Name 對象,將會導致 Person 對象也發生變化), 而是重新創建了一個 Name 對象來賦給 Person 對象的 name 屬性。當 Person 對象返回 name 屬性時,它并沒有直接返回 name 屬性的幕后字段,因為直接返回 name 屬性的幕后字段也可能導致它所引用的 Name 對象被修改。
如果將 Person 類定義改為上面形式, Person 對象的 name 的 firstName 不會被修改。

因此,如果需要設計一個不可變類,尤其要注意其引用類型的屬性,如果屬性的類型本身是可變的,就必須采取必要的措施來保護該屬性所引用的對象不會被修改,這樣才能創建真正的不可變類。

抽象類

當編寫一個類時 ,常常會為該類定義一些方法 ,這些方法用以描述該類的行為方式,這些方法都有具體的方法體。但在某些情況下,某個父類只是知道其子類應該包含怎樣的方法,但無法準確地知道這些子類如何實現方法。例如定義一個 Shape類,這個類應該提供一個計算周長的方法 calPerimeter(),但不同的Shape子類對周長的計算方法是不一樣的,即 Shape類無法準確地知道其子類計算周長的方法。
可能有人會說,既然Shape類不知道如何實現calPerimeter()方法,那就干脆不要管它了!這不是一個好思路:假設有一個 Shape變量,該變量實際上引用到 Shape子類的實例,那么這個Shape 變量就無法調用 calPerimeter()方法(必須將其強制轉換為其子類類型,才可調用calPerimeter()方法),這就降低了程序的靈活性 。

那么如何既能讓 Shape 類中包含 caIPerimeter()方法,又無須提供其方法實現呢?使用抽象方法即可滿足該要求:抽象方法是只有方法簽名,沒有方法實現的方法。
需要說明的是,有abstract修飾的成員,無須使用 open 修飾,當使用 abstract 修飾類時,表明這個類需要被繼承:當使用 abstract修飾方法、屬性時,表明這個方法、屬性必須由子類提供實現(即重寫)。而使用 final 修飾的類不能被繼承,使用 final 修飾的方法不能被重寫。 因此 final 和 abstract 永遠不能同時使用。
但是,抽象類中的具體方法、屬性依然有 final 修飾,如果程序需要重寫抽象類中的具體方法、屬性,則依然需要顯式為這些方法、屬性添加 open 修飾。

抽象成員和抽象類

抽象成員(方法和屬性)和抽象類必須使用 abstract修飾符來定義,包含抽象成員的類只能被定義成抽象類,抽象類中可以沒有抽象成員。
抽象方法和抽象類的規則如下。

  • 抽象類必須使用 abstract 修飾符來修飾,抽象成員也必須使用 abstract 修飾符來修飾,抽象方法不能有方法體。
  • 抽象類不能被實例化,無法調用抽象類的構造器創建抽象類的實例。即使抽象類中不包含任何抽象成員,這個抽象類也不能創建實例。
  • 抽象類可以包含屬性、方法(普通方法和抽象方法都可以)、構造器、初始化塊、嵌套類(接口、枚舉) 5 種成員 。 抽象類的構造器不能用于創建實例,主要用于被其子類調用。
  • 含有抽象成員的類(包括直接定義了 一個抽象成員:或繼承了一個抽象父類,但沒有完全實現父類包含的抽象成員;或實現了一個接口,但沒有完全實現接口包含的抽象成員這三種情況)只能被定義成抽象類 。

定義抽象方法,只需在普通方法上增加 abstract修飾符,并把普通方法的方法體(也就是方法后用花括號括起來的部分)全部去掉即可。
定義抽象類,只需在普通類上增加abstract修飾符即可。甚至一個普通類(沒有包含抽象方法的類)增加 abstract修飾符后也將變成抽象類。

abstract class Shape {
    init {
        println("執行 Shape 的初始化塊。。。 ")
    }

    var color = ""
    //定義一個計算周長的抽象方法
    abstract fun calPerimeter(): Double

    //定義一個代表形狀的抽象的只讀屬性
    //抽象屬性不需要初始值
    abstract val type: String

    //定義 Shape 的構造器,該構造器并不是用于創建 Shape 對象的,而是用于被子類調用
    constructor() {

    }

    constructor(color: String) {
        println("執行 Shape 的構造器...")
        this.color = color
    }

}


class Triangle(color: String, var a: Double, var b: Double, var c: Double) : Shape(color) {

    fun setSides(a: Double, b: Double, c: Double) {
        if (a >= b + c || b >= a + c || c >= a + b) {
            println("三角形兩邊之和必須大于第三邊")
            return
        }
        this.a = a
        this.b = b
        this.c = c
    }

    //重寫 Shape 類的計算周長的抽象方法
    override fun calPerimeter(): Double {
        return a + b + c
    }

    //重寫 Shape 類的代表形狀的抽象屬性
    override val type: String
        get() = "三角形"
}


class Circle(color: String, var radius: Double) : Shape(color) {
    override val type: String
        get() = "圓形"

    //重寫 Shape 類的計算周長的抽象方法
    override fun calPerimeter(): Double {
        return Math.PI * 2 * radius
    }

}

fun main(args: Array<String>) {
    var s1: Shape = Triangle("黑色", 3.0, 4.0, 5.0)
    var s2: Shape = Circle("黃色", 6.0)
    println(s1.type)
    println(s1.calPerimeter())
    println(s2.type)
    println(s2.calPerimeter())
}

abstract 不能用于修飾局部變量, Kotlin 沒有抽象變量的說法; abstract也不能用于修飾構造器,沒有抽象構造器,抽象類中定義的構造器只能是普通構造器。

使用 abstract關鍵字修飾的方法必須被其子類重寫才有意義,否則這個方法將永遠不會有方法體,因此 abstract 方法不能定義為 private 訪問權限,即 private 和 abstract不能同時修飾方法。

與 Java 類似的是, Kotlin 也允許使用抽象成員重寫非抽象成員。例如如下代碼:

open class Base1 {
    open fun foo() {}
}

abstract class Foo:Base1(){
    override abstract fun foo()
}

抽象類的作用

從前面的示例程序可以看出,抽象類不能創建實例,只能當成父類來被繼承。從語義的角度來看,抽象類是從多個具體類中抽象出來的父類,它具有更高層次的抽象。從多個具有相同特征的類中抽象出一個抽象類,以這個抽象類作為其子類的模板,從而避免了子類設計的隨意性。抽象類體現的就是一種模板模式的設計,抽象類作為多個子類的通用模板,子類在抽象類的基礎上進行擴展、改造,但子類總體上會大致保留抽象類的行為方式。

如果編寫一個抽象父類,父類提供了多個子類的通用方法,并把一個或多個方法留給其子類實現,這就是一種模板模式,模板模式也是十分常見且簡單的設計模式之一。例如前面介紹的 Shape、 Circle 和 Triangle 三個類 ,已經使用了模板模式 。下面再介紹一個模板模式的范例,在這個范例的抽象父類中,父類的普通方法依賴于一個抽象方法,而抽象方法則推遲到子類中提供實現。

//定義帶轉速屬性的主構造器
abstract class SpeedMeter(var turnRate: Double) {
    //把返問車輪半徑的方法定義成抽象方法
    abstract fun calGirth(): Double

    //定義計算速度的通用算法
    fun getSpeed(): Double {
        //速度等于車輪周長*轉速
        return calGirth() * turnRate
    }
}

class CarSpeedMeter(var radius: Double) : SpeedMeter(0.0) {
    override fun calGirth(): Double {
        return radius * 2 * Math.PI
    }

}

fun main(args: Array<String>) {
    val csm = CarSpeedMeter(0.28)
    csm.turnRate = 15.0
    println(csm.getSpeed())
}

SpeedMeter 類中提供了車速表的通用算法,但一些具體的實現細節則推遲到其子類CarSpeedMeter 中實現。這也是一種典型的模板模式。模板模式在面向對象的軟件中很常用,其原理簡單,實現也很簡單。下面是使用模板模式的一些簡單規則。

  • 抽象父類可以只定義需要使用的某些方法,把不能實現的部分抽象成抽象方法,留給其子類去實現。
  • 父類中可能包含需要調用其他系列方法的方法,這些被調方法既可以由父類實現,也可以由其子類實現。父類中提供的方法只是定義了一個通用算法,其實現也許并不完全由自身來完成,而必須依賴于其子類的輔助。

密封類

密封類這個名字很奇怪,光看名字很容易把它當成被“密封”的類,以為它不能派生子類。但實際上,密封類是一種特殊的抽象類,專門用于派生子類。
密封類與普通抽象類的區別在于: 密封類的子類是固定的。密封類的子類必須與密封類本身在同一個文件中,在其他文件中則不能為密封類派生子類,這樣就限制了在其他文件中派生子類。

在Kotlin1. 1之前,密封類的子類必須在密封類的內部聲明。

如下程序定義了 一個密封類和兩個子類 :

//定義一個密封類,其實就是抽象類
sealed class Apple {
    abstract fun taste()
}

open class RedFuji : Apple() {
    override fun taste() {
        println("紅富士蘋果香甜可口")
    }
}

data class Gala(var weight: Double) : Apple() {
    override fun taste() {
        println("嘎拉果更香脆,重量為:${weight}")
    }
}

fun main(args: Array<String>) {
    //使用 Apple 聲明變盤,用子類實例賦值
    var ap1:Apple= RedFuji()
    var ap2 :Apple = Gala(4.5)
    ap1.taste()
    ap2.taste()
}

上面程序中定義了一個密封類,接下來即可在該密封類中定義抽象方法,由此可見,密封類的本質就是抽象類。

密封類經過 Kotlin 編譯器編譯之后就得到一個抽象類的 class 文件,只不過該抽象類的普通構造器都會聲明為 private 權限,而 Kotlin 會為之創建一個對應的帶kotlin.jvm.intemal.DefaultConstructorMarker參數的構造器。

定義密封類之后,就像定義了抽象類,接下來即可在同一個文件中為該密封類派生子類,就像為普通抽象類派生子類一樣,如上面程序代碼所示。根據密封類的規則我們知道,密封類的所有構造器都必須是 private 的,不管開發者是否使用 private修飾,系統都會為之自動添加private修飾。
最后有一點需要說明的是,密封類的直接子類必須與密封類位于同一個文件中,但密封類的間接子類(子類的子類)則無須在同一個文件中。

使用密封類的好處是 : 它的子類是固定的,編譯器可以清楚地知道密封類只可能有固定數量的子類。因此使用 when表達式判定密封類時,編譯器可以清楚地知道是否覆蓋了所有情況,從而判斷是否需要添加 else 子句。

fun judge(app: Apple) {
    when (app) {
        is RedFuji -> {
            println("紅富士蘋果 ")
        }

        is Gala -> {
            println("嘎拉果")

        }
    }
}

接口

Kotlin的接口是以 Java 8接口為藍本設計的 , 因此 Kotlin 的接口與Java 8 的接口非常相似。

接口的定義

和類定義不同,定義接口時不再使用class關鍵字,而是使用 interface 關鍵字 。接口定義的基本語法如下

[修飾符) interface 接口名:父接口 1, 父接口 2... {
零個到多個屬性定義.. .
零個到多個方法定義 ...
零個到多個嵌套類、嵌套接口 、 嵌套枚舉定義 .. .
}

對上面語法的詳細說明如下:

  • 修飾符可以是 public | internal | private 中的任意一個,或完全省略修飾符。如果省略了訪問控制符,則默認采用 public。
  • 接口名應與類名采用相同的命名規則。
  • 一個接口可以有多個直接父接口,但接口只能繼承接口,不能繼承類。

與Java8相似的是,Kotlin的接口既可包含抽象方法,也可包含非抽象方法,這是非常自由的。但接口中的屬性沒有幕后字段,因此無法保存狀態,所以接口中的屬性要么聲明為抽象屬性,要么為之提供 setter、 getter方法。

由于接口定義的是一種規范,因此接口中不能包含構造器和初始化塊定義。對比接口和類的定義方式,不難發現接口中的成員比類中的成員少了兩種。
接口中定義的方法既可是抽象方法,也可是非抽象方法。如果一個方法沒有方法體, Kotlin會自動為該方法添加 abstract 修飾符;同理,如果一個只讀屬性沒有定義getter方法, Kotlin會自動為該屬性添加 abstract 修飾符;如果一個讀寫屬性沒有定義 getter、 setter 方法, Kotlin會自動為該屬性添加 abstract修飾符。
Kotlin接口與 Java接口還有一點區別:Java接口中的所有成員都會自動使用 public修飾,如果為這些成員指定訪問權限,也只能指定 public 訪問權限;但 Kotlin 接口中的成員可支持 private 和 public 兩種訪問權限 。具體規則如下:

  • 對于需要被實現類重寫的成員,如抽象方法、抽象屬性,只能使用 public 修飾。如果要添加訪問控制符,則只能用 public;如果不加訪問權限,則系統默認添加 public。
  • 對于不需要被實現類重寫的成員,如非抽象方法、非抽象屬性、嵌套類(包括嵌套抽象類)、嵌套接口、嵌套枚舉,都可使用 private 或 public 修飾,我們可以根據需要添加 private 修飾符將這些成員限制在接口內訪問:如果不加訪問權限,則系統默認添加 public。

下面定義一個接口:

interface Outputable {
    //只讀屬性定義了 getter 方法,非抽象屬性
    val name: String
        get() = "”輸出設備"
    //只讀屬性沒有定義 getter 方法,抽象屬性
    val brand: String
    //讀寫屬性沒有定義 getter、setter方法,抽象屬性
    var category: String

    //接口中定義的抽象方法
    fun out()

    fun getData(msg: String)

    //在接口中定義的非抽象方法,可使用 private 修飾
    fun print(vararg msgs: String) {
        for (msg in msgs) {
            println(msg)

        }
    }

    //在接口中定義的非抽象方法,可使用 private 修飾
    fun test(){
        println("”接口中test()方法")
    }
}

Kotlin 允許在接口中定義普通方法,上面的 Outputable 接口中定義了print()和test()兩個非抽象方法,它們都有方法體,因此系統不會自動為它們添加 abstract修飾符。由于這兩個方法是非抽象方法,因此我們可以為它們添加 private 修飾符,如果不添加任何修飾符,則系統默認為它們添加 public 修飾符 。

上面接口中還定義了三個屬性,其中 name 是一個只讀屬性,程序為該屬性定義了getter方法,因此該屬性不是抽象屬性,系統無須為之添加 abstract 修飾符: brand 是只讀屬性,沒有getter 方法,它是抽象屬性,系統會自動為之添加 abstract 修飾符; category 是讀寫屬性,程序沒有為之定義 getter 和 setter 方法,它也是抽象屬性,系統會自動為之添加 abstract 修飾符。

接口的繼承

接口的繼承和類繼承不一樣,接口完全支持多繼承,即一個接口可以有多個直接父接口。和類繼承相似,子接口繼承某個父接口,將會獲得父接口中定義的所有方法、屬性 。
一個接口繼承多個父接口時,多個父接口排在英文冒號(:)之后,它們之間以英文逗號隔開。

interface InterfaceA {
   val propA: Int
       get() = 5

   fun testA()
}

interface InterfaceB {
   val popB: Int
       get() = 6

   fun testB()
}

interface InterfaceC : InterfaceA, InterfaceB {
   val popC: Int
       get() = 7

   fun testC()
}

使用接口

接口不能用于創建實例,但可以用于聲明變量。當使用接口來聲明變量時,這個引用類型的變量必須引用到其實現類的對象。除此之外,接口的主要用途就是被實現類實現 。歸納起來,接口主要有如下用途:

  • 定義變量,也可用于進行強制類型轉換。
  • 被其他類實現。

一個類可以實現一個或多個接口,直接將被實現的多個接口、父類放在英文冒號之后,且父類、接口之間沒有順序要求,只要將它們用英文逗號隔開即可。

[修飾符] class類名: 父類, 接口1,接口2...{
類體部分
}

實現接口與繼承父類相似,一樣可以獲得所實現接口中定義的屬性(包括抽象屬性和非抽象屬性)、方法(包括抽象方法和非抽象方法)。
一個類實現了一個或多個接口之后,這個類必須完全實現這些接口中所定義的全部抽象成員(也就是重寫這些抽象方法和抽象屬性);否則,該類將保留從父接口那里繼承到的抽象成員,該類也必須定義成抽象類。
一個類實現某個接口時,該類將會獲得接口中定義的屬性、方法等 ,因此可以把實現接口理解為一種特殊的繼承,相當于實現類繼承了一個更抽象的類。

//定義一個Product接口
interface Product {
    fun getProduceTime(): Int
}

//定義一個Outputable接口
interface Outputable {
    //只讀屬性定義了 getter 方法,非抽象屬性
    val name: String
        get() = "”輸出設備"
    //只讀屬性沒有定義 getter 方法,抽象屬性
    val brand: String
    //讀寫屬性沒有定義 getter、setter方法,抽象屬性
    var category: String

    //接口中定義的抽象方法
    fun out()

    fun getData(msg: String)

    //在接口中定義的非抽象方法,可使用 private 修飾
    fun print(vararg msgs: String) {
        for (msg in msgs) {
            println(msg)
        }
    }

    //在接口中定義的非抽象方法,可使用 private 修飾
    fun test() {
        println("”接口中test()方法")
    }
}

const val MAX_CACHE_LINE = 10

//讓 Printer 類實現 Outputable 和 Product 接口
class Printer : Product, Outputable {

    private val printData = Array<String>(MAX_CACHE_LINE, { "" })

    //用以記錄當前需要打印的作業數
    private var dataNum = 0

    //重寫接口的抽象只讀屬性
    override val brand: String
        get() = "HP"

    ////重寫接口的抽象讀寫屬性
    override var category: String = "輸出外設"

    override fun getProduceTime(): Int {
        return 45
    }


    override fun out() {
        //只要還有作業,就繼續打印
        while (dataNum > 0) {
            println("打印機打印:${printData[0]}")
            //把作業隊列整體前移一位,并將剩下的作業數減1
            System.arraycopy(printData, 1, printData, 0, --dataNum)
        }
    }

    override fun getData(msg: String) {
        if (dataNum >= MAX_CACHE_LINE) {
            println("輸出隊列已滿,添加失敗")
        } else {
            //把打印數據添加到隊列里, 已保存數據的數量+1
            printData[dataNum++] = msg

        }
    }
}

fun main(args: Array<String>) {
    //創建一個 Printer 對象,當成 Output 使用
    var o: Outputable = Printer()
    o.getData("java")
    o.getData("kotlin")
    o.out()
    //調用 Outputable 接口中定義的非抽象方法
    o.print("xq", "sq", "xy")
    o.test()

    //創建一個 Printer 對象,當成 Product 使用
    val p: Product = Printer()
    println(p.getProduceTime())
    //所有接口類型的引用變盤都可直接賦給 Any 類型的變量
    val obj: Any = p

}

從上面程序中可以看出, Printer類實現了 Outputable和 Product接口,因此 Printer對象既可直接賦值給 Outputable變量,也可直接賦值給 Product變量。就好像 Printer類既是 Outputable 類的子類,也是 Product類的子類,這就是 Kotlin 提供的模擬多繼承。
上面程序中 Printer實現了 Outputable接口,即可獲取 Outputable接口中定義的 print()和 test()兩個非抽象方法,因此 Printer 實例可以直接調用這兩個默認方法。

在實現接口中的成員時,必須使用 public訪問控制符,因為接口中的成員都是 public 的,而子類(相當于實現類)重寫父類方法時訪問權限只能更大或者相等,所以實現類實現接口中的成員時只能使用 public訪問權限。

接口不能顯式繼承任何類,但所有接口類型的變量都可以直接賦給 Any 類型的變量。所以在上面程序中可以把 Product 類型的變量直接賦給 Any 類型的變量,這是利用向上轉型來實現的。因為編譯器知道任何 Kotlin對象都必須是 Any 或其子類的實例,

接口和抽象類

接口和抽象類有一些相似之處,它們都具有如下特征。

  • 接口和抽象類都不能被實例化,它們都位于繼承樹的頂端,用于被其他類實現和繼承。
  • 接口和抽象類都可以包含抽象成員,實現接口或繼承抽象類的普通子類都必須實現這些抽象成員。

但接口和抽象類之間的差別也很大,這種差別主要體現在二者的設計目的上。下面具體分析二者的差別。

接口作為系統與外界交互的窗口,體現的是一種規范。 對于接口的實現者而言,接口規定了實現者必須向外提供哪些服務(以方法、屬性的形式來提供);對于接口的調用者而言,接口規定了調用者可以調用哪些服務,以及如何調用這些服務(就是如何調用方法、訪問屬性) 。 當在一個程序中使用接口時,接口是多個模塊之間的耦合標準:當在多個應用程序之間使用接口時,接口是多個程序之間的通信標準。

從某種程度上看,接口類似于整個系統的“總綱”,它制定了系統各模塊應該遵循的標準,因此一個系統中的接口不應該經常改變。一旦接口被改變,對整個系統甚至其他系統的影響將是輻射式的,導致系統中大部分類都需要改寫 。 抽象類則不一樣,抽象類作為系統中多個子類的共同父類,所體現的是一種模板模式設計。

抽象類作為多個子類的抽象父類,可以被當成系統實現過程中的中間產品,這個中間產品已經實現了系統的部分功能(那些已經提供實現的方法),但這個產品依然不能當成最終產品,必須有更進一步的完善,這種完善可能有幾種不同方式。
此外,接口和抽象類在用法上也存在如下差別:

  • 接口中不包含構造器;但抽象類中可以包含構造器,抽象類中的構造器并不是用于創建對象的,而是讓其子類調用這些構造器來完成屬于抽象類的初始化操作。
  • 接口中不能包含初始化塊;但抽象類中則完全可以包含初始化塊。
  • 一個類最多只能有一個直接父類,包括抽象類;但一個類可以直接實現多個接口,通過實現多個接口可以彌補 Kotlin單繼承的不足。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容