1. 程式人生 > >Kotlin入門(15)獨門祕笈之特殊類

Kotlin入門(15)獨門祕笈之特殊類

上一篇文章介紹了Kotlin的幾種開放性修飾符,以及如何從基類派生出子類,其中提到了被abstract修飾的抽象類。除了與Java共有的抽象類,Kotlin還新增了好幾種特殊類,這些特殊類分別適應不同的使用場景,極大地方便了開發者的編碼工作,下面就來看看Kotlin究竟提供了哪些獨門祕笈。


巢狀類

一個類可以在單獨的程式碼檔案中定義,也可以在另一個類內部定義,後一種情況叫做巢狀類,意即A類巢狀在B類之中。乍看過去,這個巢狀類的定義似乎與Java的巢狀類是一樣的,但其實有所差別。Java的巢狀類允許訪問外部類的成員,而Kotlin的巢狀類不允許訪問外部類的成員。倘若Kotlin的巢狀類內部強行訪問外部類的成員,則編譯器會報錯“Unresolved reference: ***”,意思是找不到這個東西。下面是Kotlin定義巢狀類的程式碼例子:
class Tree(var treeName:String) {
    //在類內部再定義一個類,這個新類稱作巢狀類
    class Flower (var flowerName:String) {
        fun getName():String {
            return "這是一朵$flowerName"
            //普通的巢狀類不能訪問外部類的成員如treeName
            //否則編譯器報錯“Unresolved reference: ***”
            //return "這是${treeName}上的一朵$flowerName"
        }
    }
}
呼叫巢狀類時,得在巢狀類的類名前面新增外部類的類名,相當於把這個巢狀類作為外部類的靜態物件使用。巢狀類的呼叫程式碼如下所示:
    btn_class_nest.setOnClickListener {
        //使用巢狀類時,只能引用外部類的類名,不能呼叫外部類的建構函式
        val peachBlossom = Tree.Flower("桃花");
        tv_class_secret.text = peachBlossom.getName()
    }


內部類

既然Kotlin限制了巢狀類不能訪問外部類的成員,那還有什麼辦法可以實現此功能呢?針對該問題,Kotlin另外增加了關鍵字inner表示內部,把inner加在巢狀類的class前面,於是巢狀類華麗麗轉變為了內部類,這個內部類比起巢狀類的好處,便是能夠訪問外部類的成員。所以,Kotlin的內部類就相當於Java的巢狀類,而Kotlin的巢狀類則是加了訪問限制的內部類。按照前面演示巢狀類的樹木類Tree,也給它補充內部類的定義,程式碼如下所示:
class Tree(var treeName:String) {
    //在類內部再定義一個類,這個新類稱作巢狀類
    class Flower (var flowerName:String) {
        fun getName():String {
            return "這是一朵$flowerName"
            //普通的巢狀類不能訪問外部類的成員如treeName
            //否則編譯器報錯“Unresolved reference: ***”
            //return "這是${treeName}上的一朵$flowerName"
        }
    }

    //巢狀類加上了inner字首,就成為了內部類
    inner class Fruit (var fruitName:String) {
        fun getName():String {
            //只有宣告為內部類(添加了關鍵字inner),才能訪問外部類的成員
            return "這是${treeName}長出來的$fruitName"
        }
    }
}
呼叫內部類時,要先例項化外部類,再通過外部類的例項呼叫內部類的建構函式,也就是把內部類作為外部類的一個成員物件來使用,這與成員屬性、成員方法的呼叫方法類似。內部類的呼叫程式碼如下所示:
    btn_class_inner.setOnClickListener {
        //使用內部類時,必須呼叫外部類的建構函式,否則編譯器會報錯
        val peach = Tree("桃樹").Fruit("桃花");
        tv_class_secret.text = peach.getName()
    }


列舉類

Java有一種列舉型別,它採用關鍵字enum來表達,其內部定義了一系列名稱,通過有意義的名字比0/1/2這些數字能更有效地表達語義。下面是個Java定義列舉型別的程式碼例子:
enum Season { SPRING,SUMMER,AUTUMN,WINTER }
上面的列舉型別定義程式碼,看起來彷彿是一種新的資料型別,特別像列舉陣列。可是列舉型別實際上是一種類,開發者在程式碼中建立enum型別時,編譯器會自動生成一個對應的類,並且該類繼承自java.lang.Enum。因此,Kotlin摒棄了“列舉型別”那種模糊不清的說法,轉而採取“列舉類”這種正本清源的提法。具體到編碼上,則將enum作為關鍵字class的修飾符,使之名正言順地成為一個類——列舉類。按此思路將前面Java的列舉型別Season改寫為Kotlin的列舉類,改寫後的列舉類程式碼如下所示:
enum class SeasonType {
    SPRING,SUMMER,AUTUMN,WINTER
}
列舉類內部的列舉變數,除了可以直接拿來賦值之外,還可以通過列舉值的幾個屬性獲得對應的資訊,例如ordinal屬性用於獲取該列舉值的序號,name屬性用於獲取該列舉值的名稱。列舉變數本質上還是該類的一個例項,所以如果列舉類存在建構函式的話,列舉變數也必須呼叫對應的建構函式。這樣做的好處是,每個列舉值不但攜帶唯一的名稱,還可以擁有更加個性化的特徵描述。比如下面的列舉類SeasonName程式碼,通過建構函式能夠給列舉值賦予更加豐富的含義:
enum class SeasonName (val seasonName:String) {
    SPRING("春天"),
    SUMMER("夏天"),
    AUTUMN("秋天"),
    WINTER("冬天")
}
下面的程式碼演示瞭如何分別使用兩個列舉類SeasonType和SeasonName:
    btn_class_enum.setOnClickListener {
        if (count%2 == 0) {
            //ordinal表示列舉型別的序號,name表示列舉型別的名稱
            tv_class_secret.text = when (count++%4) {
                SeasonType.SPRING.ordinal -> SeasonType.SPRING.name
                SeasonType.SUMMER.ordinal -> SeasonType.SUMMER.name
                SeasonType.AUTUMN.ordinal -> SeasonType.AUTUMN.name
                SeasonType.WINTER.ordinal -> SeasonType.WINTER.name
                else -> "未知"
            }
        } else {
            tv_class_secret.text = when (count++%4) {
                //使用自定義屬性seasonName表示更個性化的描述
                SeasonName.SPRING.ordinal -> SeasonName.SPRING.seasonName
                SeasonName.SUMMER.ordinal -> SeasonName.SUMMER.seasonName
                SeasonName.AUTUMN.ordinal -> SeasonName.AUTUMN.seasonName
                SeasonName.WINTER.ordinal -> SeasonName.WINTER.seasonName
                else -> "未知"
                //列舉類的建構函式是給列舉型別使用的,外部不能直接呼叫列舉類的建構函式
                //else -> SeasonName("未知").name
            }
        }
    }


密封類

前面演示外部程式碼判斷列舉值的時候,when語句末尾例行公事加了else分支。可是列舉類SeasonType內部一共只有四個列舉變數,when語句有四個分支就行了,最後的else分支純粹是多此一舉。出現此種情況的緣故是,when語句不曉得SeasonType只有四種列舉值,因此以防萬一必須要有else分支,除非編譯器認為現有的幾個分支已經足夠。
為解決列舉值判斷的多餘分支問題,Kotlin提出了“密封類”的概念,密封類就像是一種更加嚴格的列舉類,它內部有且僅有自身的例項物件,所以是一個有限的自身例項集合。或者說,密封類採用了巢狀類的手段,它的巢狀類全部由自身派生而來,彷彿一個家譜明明白白列出來某人有長子、次子、三子、么子。定義密封類時使用關鍵字sealed標記,具體的密封類定義程式碼如下所示:
sealed class SeasonSealed {
    //密封類內部的每個巢狀類都必須繼承該類
    class Spring (var name:String) : SeasonSealed()
    class Summer (var name:String) : SeasonSealed()
    class Autumn (var name:String) : SeasonSealed()
    class Winter (var name:String) : SeasonSealed()
}
有了密封類,通過when語句便無需指定else分支了,下面是判斷密封類物件的程式碼例子:
    btn_class_sealed.setOnClickListener {
        var season = when (count++%4) {
            0 -> SeasonSealed.Spring("春天")
            1 -> SeasonSealed.Summer("夏天")
            2 -> SeasonSealed.Autumn("秋天")
            else -> SeasonSealed.Winter("冬天")
        }
        //密封類是一種嚴格的列舉類,它的值是一個有限的集合。
        //密封類確保條件分支覆蓋了所有的列舉型別,因此不再需要else分支。
        tv_class_secret.text = when (season) {
            is SeasonSealed.Spring -> season.name
            is SeasonSealed.Summer -> season.name
            is SeasonSealed.Autumn -> season.name
            is SeasonSealed.Winter -> season.name
        }
    }


資料類

在Android開發中,免不了經常定義一些存放資料的實體類,比如使用者資訊、商品資訊等等,每逢定義實體類之時,開發者基本要手工完成以下編碼工作:
1、定義實體類的每個欄位,以及對欄位進行初始賦值的建構函式;
2、定義每個欄位的get/set方法;
3、在判斷兩個資料物件是否相等時,通常每個欄位都比較一遍;
4、在複製資料物件時,如果想修改某幾個欄位的值,得再補充對應數量的賦值語句;
5、在除錯程式時,為獲知資料物件裡儲存的欄位值,得手工把每個欄位值都打印出來;
如此折騰一番,僅僅是定義一個實體類,開發者就必須完成這些繁瑣的任務。然而這些任務其實毫無技術含量可言,如果每天都在周而復始地敲實體類的相關編碼,毫無疑問跟工地上的搬磚民工差不多,活生生把程式設計師弄成一個拼時間拼體力的職業。有鑑於此,Kotlin再次不負眾望推出了名為“資料類”的特殊類,直接戳中程式設計師事多、腰痠、睡眠少的痛點,極大程度上將程式設計師從無涯苦海中拯救出來。
資料類說神祕也不神祕,它的類定義程式碼極其簡單,只要開發者在class前面增加關鍵字“data”,並宣告入參完整的建構函式,即可無縫實現以下功能:
1、自動宣告與構造入參同名的屬性欄位;
2、自動實現每個屬性欄位的get/set方法;
3、自動提供equals方法,用於比較兩個資料物件是否相等;
4、自動提供copy方法,允許完整複製某個資料物件,也可在複製後單獨修改某幾個欄位的值;
5、自動提供toString方法,用於列印資料物件中儲存的所有欄位值;
功能如此強大的資料類,猶如葵花寶典,讓你功力倍增。見識了資料類的深厚功力,再來看看它的類程式碼是怎麼定義的:
//資料類必須有主建構函式,且至少有一個輸入引數,
//並且要宣告與輸入引數同名的屬性,即輸入引數前面新增關鍵字val或者var,
//資料類不能是基類也不能是子類,不能是抽象類,也不能是內部類,更不能是密封類。
data class Plant(var name:String, var stem:String, var leaf:String, var flower:String, var fruit:String, var seed:String) {
}
想不到吧,原來資料類的實現程式碼竟然如此簡單,當真是此時無招勝有招。當然,為了達到這個程式碼精簡的效果,資料類也得遵循幾個規則,或者說是約束條件,畢竟不以規矩不成方圓,正如類定義程式碼所註釋的那樣:
1、資料類必須有主建構函式,且至少有一個輸入引數,因為它的屬性欄位要跟輸入引數一一對應,如果沒有屬性欄位,這個資料類儲存不了資料也就失去存在的意義了;
2、主建構函式的輸入引數前面必須新增關鍵字val或者var,這保證每個入參都會自動宣告同名的屬性欄位;
3、資料類有自己的一套行事規則,所以它只能是個獨立的類,不能是其他型別的類,否則不同規則之間會產生矛盾;
現在利用上面定義的資料類——植物類Plant,演示看看外部如何操作資料類,具體呼叫程式碼如下所示:
    var lotus = Plant("蓮", "蓮藕", "蓮葉", "蓮花", "蓮蓬", "蓮子")
    //資料類的copy方法不帶引數,表示複製一模一樣的物件
    var lotus2 = lotus.copy()
    btn_class_data.setOnClickListener {
        lotus2 = when (count++%2) {
            //copy方法帶引數,表示指定引數另外賦值
            0 -> lotus.copy(flower="荷花")
            else -> lotus.copy(flower="蓮花")
        }
        //資料類自帶equals方法,用於判斷兩個物件是否一樣
        var result = if (lotus2.equals(lotus)) "相等" else "不等"
        tv_class_secret.text = "兩個植物的比較結果是${result}\n" +
                "第一個植物的描述是${lotus.toString()}\n" +
                "第二個植物的描述是${lotus2.toString()}"
    }


模板類

在前面的文章《Kotlin入門(11)江湖絕技之特殊函式》中,提到了泛型函式,當時把泛型函式作為全域性函式定義,從而在別的地方也能呼叫它。那麼如果某個泛型函式在類內部定義,即變成了這個類的成員方法,又該如何定義它呢?這個問題在Java中是通過模板類(也叫做泛型類)來解決的,例如常見的容器類ArrayList、HashMap均是模板類,Android開發中的非同步任務AsyncTask也是模板類。
模板類的應用如此廣泛,Kotlin自然而然保留了它,並且寫法與Java類似,一樣在類名後面補充形如“<T>”或者“<A, B>”的表示式,表示這裡的型別待定,要等建立類例項時再確定具體的變數型別。待定的型別可以有一個,如ArrayList;可以有兩個,如HashMap;也可以有三個或者更多,如AsyncTask。舉個例子,森林裡有一條小河,小河的長度可能以數字形式輸入(包括Int、Long、Float、Double),也可能以字串形式輸入(String型別)。如果輸入的是數字長度,則長度單位採取“m”;如果輸入的是字串長度,則長度單位採取“米”。按照以上需求編寫名為River的模板類,具體的類定義程式碼如下:
//在類名後面新增“<T>”,表示這是一個模板類
class River<T> (var name:String, var length:T) {
    fun getInfo():String {
        var unit:String = when (length) {
            is String -> "米"
            //Int、Long、Float、Double都是數字型別Number
            is Number -> "m"
            else -> ""
        }
        return "${name}的長度是$length$unit。"
    }
}
外部呼叫模板類建構函式的時候,要在類名後面補充“<引數型別>”,從而動態指定實際的引數型別。不過正如宣告變數那樣,如果編譯器能夠根據初始值判斷該變數的型別,就無需顯式指定該變數的型別;模板類也存在類似的偷懶寫法,如果編譯器根據輸入引數就能知曉引數型別,則呼叫模板類的建構函式也不必顯式指定引數型別。以下是外部使用模板類的程式碼例子:
    btn_class_generic.setOnClickListener {
        var river = when (count++%4) {
            //模板類(泛型類)宣告物件時,要在模板類的類名後面加上“<引數型別>”
            0 -> River<Int>("小溪", 100)
            //如果編譯器根據輸入引數就能知曉引數型別,也可直接省略“<引數型別>”
            1 -> River("瀑布", 99.9f)
            //當然保守起見,新手最好按規矩新增“<引數型別>”
            2 -> River<Double>("山澗", 50.5)
            //如果你已經是老手了,怎麼方便怎麼來,Kotlin的設計初衷就是偷懶
            else -> River("大河", "一千")
        }
        tv_class_secret.text = river.getInfo()
    }


總結一下,本文介紹了Kotlin的六種特殊函式,首先巢狀類和內部類都定義在某個外部類的內部,區別在於能否訪問外部類的成員;其次列舉類和密封類都提供了有序的列舉值集合,區別在於密封類的定義更加嚴格;再次是幫助開發者擺脫搬磚命運的資料類;最後是解決未定引數型別的模板類(也叫泛型類)。


點此檢視Kotlin入門教程的完整目錄


__________________________________________________________________________
本文現已同步釋出到微信公眾號“老歐說安卓”,開啟微信掃一掃下面的二維碼,或者直接搜尋公眾號“老歐說安卓”新增關注,更快更方便地閱讀技術乾貨。