Kotlin 總結系列(2)——函數和類

kotlin總結系列(1)--基礎要素java

一 函數的定義和調用

主要是kotlin上函數的特色:命名參數、默認參數值、頂層函數和屬性、擴展方法和擴展屬性(本質上是靜態函數高級語法糖),和能消除重複代碼(DRY)的局部函數。數據庫

讓咱們從一個經常使用例子出發,java集合都有默認的toString()方法,但它的輸出是固定格式化的,有時並非你所須要的([1,2,3]),如要自定義字符串的前綴、後綴,和分隔符時,通常定義個方法,並傳入參數:編程

fun <T> joinToString(collection: Collection<T>,separator:String,prefix:String,postfix:String):String{
    val result = StringBuilder(prefix)
    for ((index,element) in collection.withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
複製代碼

使用:json

val list = listOf("kotlin","java")
println(joinToString(list,",","{","}")) // {kotlin,java}
複製代碼

1 命名參數

關注的第一個問題就是函數的可讀性,如上面的使用joinToString(list,",","{","}"),難以看出這些string對應的是什麼參數(雖然能夠藉助ide)。kotlin能夠優雅的調用,便可以顯式的標明一些參數的名稱,如joinToString(list,",",prefix = "{",postfix = "}"),直接在調用時指明瞭prefixpostfix,能清晰明瞭的分辨出來。bash

注: 爲避免混淆,當指明瞭一個參數名稱後,那它以後的全部參數都要顯式指明名稱app

2 默認參數值

java函數的另外一個廣泛存在的問題是,一些類的重載函數實在太多了(如java.lang.Thread便有8個構造方法),致使參數名和類型被不斷重複。ide

kotlin則使用了默認參數值,能夠在聲明函數的時候,指定參數的默認值,就能夠避免重複建立函數。如上面的joinToString函數,大多數狀況下,能夠不加前綴或者後綴並用逗號分隔,因此把它們設爲默認值:函數

//只改變了參數聲明
fun <T> joinToString(collection: Collection<T>,separator:String=",",prefix:String="",postfix:String=""):String{
    val result = StringBuilder(prefix)
    for ((index,element) in collection.withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
複製代碼

能夠像java多重重載函數同樣地調用:工具

println(joinToString(list)) // 打印:kotlin,java
println(joinToString(list,";")) // 打印:kotlin;java
println(joinToString(list,prefix = "{",postfix = "}"))  // 打印:kotlin;java 
複製代碼

3 消除靜態工具類的頂層函數和屬性

java中,全部的代碼都必須寫成類的函數。有時存在一個基本的對象,但你不想經過實例函數來添加操做,讓它的API繼續膨脹,結果就是,會把這些函數寫成靜態,並交由不包含任何狀態和實例函數的類保管,如JDK中的Collections,或者本身代碼中,一些以Util做爲後綴的工具類。post

kotlin能夠直接把函數或屬性放在代碼文件頂層,而不用從屬於任何的類。(依然是包內成員,若是須要從包外訪問它,則須要import(可使用as更改導入的名字),但再也不須要額外包一層。

kotlin屬性也能夠放在文件頂層,相似java靜態字段; 若想像java的public final static同樣聲明一個常量,kotlin可使用const val修飾

使用方式在下面給出↓

4 擴展方法和屬性

4.1 擴展方法

kotlin的一大特點是,能夠平滑地與現有代碼集成。擴展函數能夠在類的外面定義一個類的成員函數,如咱們添加一個方法擴展String類型,計算一個字符串的最後一個字符並返回:(像成員函數同樣地調用

import...

fun String.lastChar():Char = this.get(length-1)  //this能夠省略

...
fun main(){
    //像成員函數同樣地調用
    println("kotlin".lastChar()) // 打印: n
}
複製代碼

擴展函數的聲明,與普通函數區別就是,把你要擴展的類或者接口名稱,反到即將添加的函數名簽名,這個類被稱爲接收者類型,如例子中的String;用來調用這個擴展函數的那個對象,被叫作 接收者對象,如例子中的「kotlin」

在擴展函數中,能夠直接訪問被擴展類或接口的其餘方法和屬性(如例子中的String.get方法),就好像是在這個類中定義同樣。

對於開始的字符串例子,如今能夠這麼定義使用:

fun <T> Collection<T>.joinToString(separator:String=",",prefix:String="",postfix:String=""):String{
    val result = StringBuilder(prefix)
    for ((index,element) in withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

...
val list = listOf("kotlin","java")
println(list.joinToString(prefix = "{",postfix = "}"))  //kotlin;java

複製代碼

擴展函數本質上,是java的靜態函數的高效語法糖,最終是被轉爲一個接收該對象類型的靜態函數,所以是不能被繼承重寫的。

注: 當類的成員函數和擴展函數有相同簽名時,成員函數會優先使用

4.2 擴展屬性

和擴展函數同樣,kotlin也支持擴展屬性,使用相似,如:

var StringBuilder.lastChar:Char
    get() = get(length -1)
    set(value) {
        this.setCharAt(length-1,value)
    }
    
...
val sb = StringBuilder("kotlin?")
println(sb.lastChar) // 打印: ?
sb.lastChar = '!'
println(sb) // 打印: kotlin!
    
複製代碼

注: 擴展屬性是沒有支持字段存儲的

5 局部函數

提升代碼質量標準之一:不要重複你本身的代碼(DRY)。kotlin提供了一個整潔的方案,局部函數:能夠在函數中嵌套函數

讓咱們來看怎麼用局部函數解決常見的代碼重複問題,例子中,saveUser函數用於將user信息存到數據庫中,並確保user對象含有有效數據

class User(val id:Int,val name:String,val address:String)
複製代碼
fun saveUser(user:User){
    if (user.name.isEmpty()){
        throw IllegalArgumentException("Can't save user ${user.id} :empty Name")
    }
    if (user.address.isEmpty()){
        throw IllegalArgumentException("Can't save user ${user.id} :empty Address")
    }

    //保存到數據庫中
    ...
}
複製代碼

函數saveUser中,存在重複的字段檢查,當檢查字段增多,代碼會顯得特別臃腫,可利用局部函數提取重複代碼:

fun saveUser(user:User){
    //可省略局部函數的user參數
    fun validate(user: User,value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
        }
    }
    validate(user,value = user.name,fieldName = "Name")
    validate(user,value = user.address,fieldName = "Address")

    //保存到數據庫中
}
複製代碼

聲明瞭一個局部函數validate提取重複的檢查邏輯,因局部函數能夠訪問到所在函數的全部參數和變量,因此,能夠去掉冗餘的User參數:

fun saveUser(user:User){
    //去掉冗餘的User參數
    fun validate(value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
        }
    }
    validate(value = user.name,fieldName = "Name")
    validate(value = user.address,fieldName = "Address")

    //保存到數據庫中
}
複製代碼

繼續改進,能夠將user的驗證擴展成擴展函數

fun User.validateBeforeSave(){
    fun validate(value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user $id :empty $fieldName")
        }
    }
    
    validate(value = name,fieldName = "Name")
    validate(value = address,fieldName = "Address")
}

fun saveUser(user:User){

   user.validateBeforeSave()

    //保存到數據庫中
}
複製代碼

再次代表,擴展函數能夠很大程度優化代碼。

注:擴展函數也能夠被聲明爲局部函數,但通常不建議多層嵌套,因深度嵌套的局部函數每每會讓人太費解

二 類和接口

kotlin接口和類的實現與java仍是有一點區別的,如接口能夠包含屬性聲明;kotlin的聲明默認是public final 的;此外,嵌套類默認不是內部類,即靜態的,沒有包含懟外部類的隱式引用

1 類的繼承結構

1.1 kotlin中的接口

kotlin中的接口與java8中的類似,能夠包含抽象方法,和非抽象方法的實現(與java8中默認方法相似);同時能夠有屬性聲明,但沒有包含任何狀態,即沒有支持字段來保存(後面有講)

接口聲明:

interface Clickable{
    fun click()  //抽象方法
    fun showOff() = println("I'm clickable") //帶默認實現的方法
}
複製代碼

kotlin在類名後面用冒號來代替java的extends和implements關鍵字。和java同樣,一個類能夠實現任意多個接口,但只能繼承一個類

kotlin用override修飾符標註被重寫的方法和屬性,同時,override修飾符是強制要求

class Button:Clickable{
    override fun click() {
        println("button clicked")
    }

}
複製代碼

若同時實現兩個接口,且兩個接口含有相同的函數簽名,且都有默認實現,則kotlin會強制要求提供你本身的實現

interface Clickable{
    fun click()
    fun showOff() = println("I'm clickable")
}

interface Focusable{
    fun focus()
    fun showOff() = println("I'm focusable")
}

class Button:Clickable,Focusable{

    override fun click() {
        println("button clicked")
    }

    override fun focus() {
        println("button clicked")
    }

    override fun showOff() {
    //使用尖括號加父類名字的「super」代表了你想要調用哪一個父類方法
        super<Clickable>.showOff() 
        super<Focusable>.showOff()
    }
}
複製代碼

調用父類實現,kotlin也使用了關鍵字super,使用尖括號加父類名字的「super」代表了你想要調用哪一個父類方法

1.2 關於重寫基類的修飾符,open、final和abstract:默認是final

kotlin中,類,方法和屬性默認是final的,即不可重寫。若是要容許建立子類,須要使用open修飾符來標示這個類,屬性或方法也要添加。

open class RichButton:Clickable{ //這個類是open的,便可繼承的
    fun disable(){}  //這個函數是final的,子類不可重寫
    
    open fun animate(){} //這個函數是open的,子類可重寫

    override fun click() {} //重寫的成員默認是open的,除非顯式標註 final
}
複製代碼

注: 重寫的成員默認是open的,除非顯式標註爲final

同java同樣,能夠將一個類聲明爲abstract,這種類能夠有一些沒被實現而且須要在子類實現的抽象成員(用abstract修飾),抽象成員不能有實現,與java基本一致。

注: abstract始終是open的,同時,接口也始終是open的,都不能聲明爲final。

修飾符 相關成員 批註
final 不能被重寫 類中成員默認使用
open 能夠被重寫 須要明確標示
abstract 必須被重寫 只能在抽象類中使用;抽象成員不能有實現
override 重寫父類或接口中的成員 若是沒有使用final明確代表,重寫的成員默認是open的

1.3 可見性修飾符:默認是public

與java可見性區別:

  • (1)kotlin中,默認是public
  • (2)Java的默承認見性——包私有,在kotlin中並無,取而代之的是新的修飾符internal,表示「只在模塊內部可見」
  • (3)kotlin容許在頂層聲明中使用private可見性,包括類、函數,接口和屬性,這種聲明就會只在聲明它們的文件中可見
  • (4)kotlin禁止去引用低可見的類型
  • (5)kotlin中,protected成員只能在類和它的子類中可見;同時,類的擴展函數或屬性不能訪問它的private和protected成員(因擴展函數或屬性時靜態函數高級語法糖)
  • (6)kotlin中,外部類不能訪問到其內部(或嵌套)類中的private和protected成員
修飾符 類成員 頂層聲明
public(默認) 全部地方可見 全部地方可見
internal 模塊中可見 模塊中可見
protected 類和子類中可見 ----
private 類中可見 文件中可見

1.4 內部類和嵌套類:默認是嵌套類

kotlin中,默認嵌套類不能訪問外部類實例,即至關於靜態內部類,沒有隱式擁有外部類的實例。若是要把變成一個內部類來持有一個外部類的引用的話,須要使用inner修飾符。在kotlin中引用外部類實例語法也與java不一樣,需使用this@Outer(java是Outer.this)

class Outer{
    inner class Inner{ //聲明爲inner
        fun getOuterReference():Outer = this@Outer //引用外部類實例
    }
}
複製代碼

1.5 密封類

密封類:包含有限數量的類的繼承結構。

在使用when表達式的時候,老是提供一個else分支很不方便,若是處理的是sealed類的子類,則能夠再也不須要提供默認分支,且當sealed添加一個子類時,有返回值的when表達式會編譯失敗,並告訴你哪裏必須修改

sealed class Expr{
    class Num:Expr() //括號構造方法下面講解
    class Sum:Expr()
    class Plus:Expr()
}

data class Out(val s:String) :Expr() //後續添加的子類

fun eval(e:Expr):String{
    when(e){
        is Expr.Num -> return "num"
        is Expr.Sum -> return "Sum"
        is Expr.Plus -> return "plus"
        is Out-> return "plus" //必須把後續添加的子類放到分支裏,不然報錯
    }
}
複製代碼

注: sealed修飾的這個類始終是open的。且不能用於聲明sealed接口

2 類的構造方法和自定義getter/setter屬性

類的構造方法區分了主構造方法從構造方法。同時也容許在初始化語句塊中添加額外的初始化邏輯

2.1 主構造方法和初始化語句塊

一個簡單類的聲明:

class User(val nickname:String)
複製代碼

這段被括號圍起來的語句塊就叫作主構造方法,主要兩個目的,代表構造方法參數和使用這些參數初始化的屬性。完成一樣事情最明確代碼以下:

class User constructor(_nickname:String){ //帶一個參數的主構造方法
    val nickname:String //屬性
    
    init {    //初始化代碼塊
        nickname = _nickname
    }
}
複製代碼
  • (1)關鍵字constructor用來開始一個主構造方法或從構造方法的聲明;而init關鍵字用來引入一個初始化語句塊,這種語句塊包含了類被建立時執行的代碼,能夠在一個類中聲明多個初始化語句塊
  • (2)這個例子能夠省略init,直接初始化屬性,同時若是主構造方法沒有註解或可見性修飾符,能夠省略constructor關鍵字
class User(_nickname:String){
    val nickname = _nickname
}
複製代碼
  • (3)能夠把val/var關鍵字放在參數前進行簡化,同時能夠指定參數默認值
class User(val nickname:String = "John")
複製代碼

對於子類,須要初始化父類,可在類聲明中使用父類的構造方法:

open class Button

class RadioButton:Button()
複製代碼

這就是爲何在父類名稱後面還須要一個空的括號;接口沒有構造方法,因此不用加括號

2.2 從構造方法:用不一樣方式來初始化類

open class View{
    constructor(context:Context?){ //從構造方法
        println("this is view1")
    }

    constructor(context: Context?,attr:AttributeSet?){
        println("this is view2")
    }
}
複製代碼
  • 若是沒有主構造方法,那麼每一個構造方法必須初始化基類或委託給另外一個這樣作了的構造方法
  • 主構造方法優先,若是同時有主從構造方法,從構造方法需顯式調用this(...)以知足主構造方法
class MyView:View{

    constructor(context: Context?):this(context,null){ //this委託
        println("this is MyView1")
    }

    constructor(context: Context?,attr: AttributeSet?):super(context,attr){ //初始化基類
        println("this is MyView2")
    }
}
複製代碼

2.3 接口或抽象類中的屬性

  • 1 kotlin接口是能夠含有屬性聲明的。但不能有狀態,即沒有支持字段存儲
interface User{
    val nickName:String
}
複製代碼

重寫屬性有三種方式

class User1(override val nickName: String) :User //有支持字段存儲

class User2(val email:String):User{
    override val nickName: String //沒有支持字段存儲
        get() = email.substringBefore("@")
}

class User3(val id:Int):User{ 
    override val nickName = "$id" //有支持字段存儲
}
複製代碼
  • 2 接口除了抽象屬性聲明外,也能夠含有具備getter/setter的屬性,只要他們沒引用支持字段,如:
interface User{
    val email:String
    val nickName:String //並無持有支持字段和狀態
        get() = email.substringBefore("@")
}
複製代碼
  • 3 經過getter或setter訪問支持字段

屬性能夠自定義訪問器getter/setter以提供被訪問或修改時額外邏輯。假設須要在修改時輸出日誌:

class User(val name:String){
    var address = "unSpecified"
        set(value) {
            println("address was changed")
            field = value // filed表示支持的字段值
        }
}
複製代碼

field標識符在getter/setter表示支持字段的值,在getter中,只能讀取值,在setter中,既能讀取也能修改。

注: 能夠只修改一個訪問器,另外一個會自動用默認的實現

  • 4 有無支持字段的判斷:訪問屬性的方式不依賴因而否含有支持字段。若是顯式引用或者使用默認訪問器實現,編譯器會爲屬性生產支持字段。若是提供了一個自定義的訪問器而且沒有使用field(若是是val,就是getter,若是是var,就是getter和setter),支持字段就不會被呈現出來。

  • 5 訪問器的可見性默認與屬性的可見性相同,但能夠修改,如:

class User(val name:String){
    var address = "unSpecified"
        private set //修改setter的可見性爲private
}
複製代碼

3 data class 和by委託

4 對象聲明

  • 1 object關鍵字:對象聲明,定義一個類,並同時建立一個實例(單例
  • 2 與普通類同樣,一個對象能夠含有屬性,方法,初始化語句塊等,惟一不一樣的是,不容許有構造函數
  • 3 對象也能夠繼承自類或接口,一樣也能夠在類中聲明
  • 4 本質上被編程成了經過靜態字段持有的的單一實例類
data class Person(val name:String){

    object NameComparator:Comparator<Person>{  //單一實例
        override fun compare(p0: Person?, p1: Person?): Int {
            if (p0 ==null || p1 == null)
            return 0
            return p0.name.compareTo(p1.name)
        }
    }
}

...
//使用
Person.NameComparator.compare(Person("John"),Person("Cap"))

複製代碼

5 伴生對象:替代工廠方法和靜態成員

kotlin中的類不能擁有靜態成員,java的static關鍵字並非kotlin語言的一部分。做爲替代,通常使用頂層函數和對象聲明。大多數狀況下推薦頂層函數,但頂層函數沒法訪問private成員,像工廠方法這種例子就得使用對象聲明。

  • 1 kotlin中,在類中聲明的對象可使用特殊關鍵字標記:companion。從而能夠直接經過容器名稱來訪問這個對象的方法和屬性,最終看起來就像是java的靜態方法調用:
class A{
    companion object{
        fun bar() = println("called")
    }
}

...
A.bar() // 輸出 called,像靜態方法同樣調用

複製代碼

:本質上也是被編程成了經過靜態字段持有的的單一實例類

  • 2 伴生對象能夠訪問類中的private成員,包括private構造方法,是實現工廠方法的理想選擇
class User private constructor(val name: String){
    companion object{
        fun newFaceBookUser(email:String) = User(email.substringBefore('@'))
        
        fun newMarvelUser() = User("Cap")
    }
}

複製代碼

能夠經過類名來調用companion object的方法

val facebook = User.newFaceBookUser("xxx@163.com")
val marvel = User.newMarvelUser()
複製代碼
  • 3 伴生對象是一個聲明在類中的普通對象聲明,能夠有名字(不指定則默認爲 companion
class Person(val name:String){
    companion object Loader{
        fun fromJSON(jsonText:String):Person = ...
    }
}
複製代碼

則能夠經過兩種方式使用

val person = person.Loader.fromJSON(...)
val person2 = person.fromJSON(...)
複製代碼
  • 4 伴生對象能夠像其餘對象聲明同樣,實現接口,因伴生對象的特性,能夠直接將包含它的類的名字做爲實現了改接口的對象實例來使用
interface JSONFactory<T>{
    fun fromJSON(json:String):T
}

class Hero private constructor(val heroName:String){
    companion object Loader:JSONFactory<Hero>{
         override fun fromJSON(json: String): Hero {
            return Hero("IronMan")
        }
    }
}

fun <T> loadFromJson(jsonFactory: JSONFactory<T>) = jsonFactory.fromJSON("John")

//使用,直接將包含它的類的名字做爲實現了改接口的對象實例來使用
 loadFromJson(Hero)
複製代碼
  • 5 伴生對象能夠像普通對象同樣使用擴展函數或屬性,接收者類型顯式指出伴生對象名字便可
class Person(val name:String){
    companion object{
    }
}

//擴展函數顯式指明默認名字companion
fun Person.companion.fromJSON(json:String):Person{ 
    ...
}
複製代碼

使用時,仍能夠直接用包含的類名來調用

val p = Person.fromJSON(json)
複製代碼

6 對象表達式:kotlin的匿名內部類寫法

object關鍵字不只能用來聲明單例對象,還能用來聲明匿名對象

與java匿名對象不一樣的是:

  • 1 與java匿名對象只能擴展一個類或接口不用,kotlin的匿名對象能夠實現多個接口或不實現接口
  • 2 與對象聲明不一樣,匿名對象不是單例的,每次對象表達式被執行,都會建立一個新的對象實例
  • 3 與java匿名對象同樣,對象表達式代碼能夠訪問建立它的函數中的變量,但不用的是,訪問沒有被限制在final變量之中
class MainActivity : AppCompatActivity() {

    val c:String = ""
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var clickCount = 0
        
        val view :View= findViewById(R.id.action)
        
        //這裏的匿名對象表達式能夠被lambda代替
        view.setOnClickListener(object :View.OnClickListener{ 
            override fun onClick(p0: View?) {
                clickCount ++ //不須要final
                println(c)
            }
        })
    }
}
複製代碼

匿名對象也能夠直接存儲到一個變量中

val listener = object :View.OnClickListener{ 
            override fun onClick(p0: View?) {
                ...
            }
        }
複製代碼
相關文章
相關標籤/搜索