Kotlin基礎知識
一、 類
1. 可以聲明一個空類
class Empty
java 中即使是空類,也需要寫類體
2. 構造器
關鍵字constructor,當主構造函數沒有任何注解或者可見性修飾符,可以省略。
class Person constructor(name: String, age: Int)
等價於
class Person(name: String, age:
Int)
3. 主構造函數與次構造函數
主構造函數:定義在類頭中的構造函數。
次構造函數:定義在類體中的構造函數。
在 Kotlin 中,類中可以聲明主構造函數(零個或一個)和次構造函數(零個或多個)。次構造函數可以使用this關鍵字調用其他的構造函數,沒有主構造函數也可以寫次構造函數,這時次構造函數不需要委托給主構造函數(無主構造函數)。
l 主構造函數隻能調用父類的構造函數
l 次構造函數可以調用其他次構造函數(次構造函數不能循環調用),但是如果存在主構造函數,最終需要調用主構造函數。
l 如果一個類中,沒有主構造函數,但是有init初始化塊和次構造函數,那麼在調用這個類的次構造函數時,init初始化塊會在次構造函數執行前執行。
在主構造函數中定義的屬性,如果有var或val修飾,就相當於類的屬性,可以在類內部使用,如果沒有修飾,則隻能在init初始化塊和類體內聲明的屬性初始化器中使用。次構造函數中的參數不能用val與var修飾。
類頭中聲明的主構造函數中不能包含任何的代碼,隻能放在init初始化塊中。
Q:與swift的區別?
l kotlin中隻能有一個主構造器,多個次構造器。
l swift中可以有多個指定構造器,多個便利構造器。
Q:kotlin中為什麼有主構造函數和次構造函數之分?
個人理解:在主構造函數中,可以聲明並初始化類中全部的存儲屬性,可以簡化類結構。而且次構造函數最終需要調用主構造函數,這樣可以保證類中的存儲屬性都可以完成初始化。但如果次構造函數也可以聲明成員屬性,那麼就不能保證所有的屬性都可以初始化,類結構會比較混亂。所以構造函數需要有主次之分。
沒有主構造函數,隻有多個次構造函數的情況,是為了方便從java轉到kotlin開發的程序員使用。
4. 可以使用無參方式調用構造函數初始化實例對象的條件:
下列條件滿足一條即可:
l 類中沒有任何一個構造函數(主構造函數、次構造函數)
l 主構造函數所有參數都有默認值。
l 有一個次構造函數所有參數都有默認值。
注意:
a) 隻有主構造函數的參數全部都有默認值的情況下,編譯器才會真正的生成一個無參構造方法,此時,通過無參方式調用構造函數創建實例對象時,會調用有默認值的有參構造函數,並不會調用額外生成的無參構造函數,這個額外的無參構造函數主要是給java代碼中用的。
b) 次構造函數的參數都有默認值的時候,不會額外生成無參構造函數,隻是在調用的時候,使用參數的默認值。
5. 利用可見性修飾符修飾主構造函數
默認可見為public,可以用private修飾 ---->引申,java中單例
Q:主構造函數用private修飾,並且無參數,當有一個次構造函數全部參數都有默認值時,會怎樣?
A:在代碼中可以使用無參構造函數創建對象。應該避免這麼做!!!
二、 屬性和字段
1. 聲明屬性
在kotlin中屬性分為隻讀屬性和可變屬性兩種,隻讀屬性用關鍵字val聲明,可變屬性用關鍵字var 聲明。
2. Getters 和 Setters
kotlin中可以給屬性設置自定義的get和set訪問器。多用於計算屬性。
get與set可以任意設置,隻有get、隻有set或者都有。
l 當getter可以推斷出屬性類型時,可以省略類型聲明。
l 在get與set內部,不可以用屬性名本身執行語句(互相引用,導致棧溢出),如果想使用本身屬性值時,需要用幕後字段field。
l 可以對get與set進行可見性修飾和加注解
a) getter必須與屬性可見性一致
b) setter可以隨意設置,但是不會超出類的可見性
get() = 可以使用函數執行結果賦值
set(value) = 後麵可以加if、when、try/catch表達式
3. 幕後字段backing field
Kotlin 中類不能有字段。然而,當使用自定義訪問器時,有時有一個幕後字段(backing field)有時是必要的。為此 Kotlin 提供一個自動幕後字段,它可通過使用 field 標識符訪問。
field 標識符隻能用在屬性的訪問器內。
如果屬性至少一個訪問器使用默認實現,或者自定義訪問器通過 field 引用幕後字段,將會為該屬性生成一個幕後字段。
4. 幕後屬性
個人理解:類似於幕後字段的手動實現,可控性強。
5. 編譯期常量
已知值的屬性可以使用 const 修飾符標記為編譯期常量,可以使用在注解中。這些屬性需要滿足以下要求:
l 指定定義在頂層、 object或伴生對象中;
l 用 String 或原生類型 值初始化;
l 沒有自定義 getter。
6. 延遲初始化屬性
關鍵字lateinit,該修飾符隻能用於在類體中(不是在主構造函數中)聲明的 var 屬性,並且僅當該屬性沒有自定義 getter 或 setter 時。該屬性必須是非空類型,並且不能是原生類型。
在使用延遲初始化屬性之前,必須要初始化,否則將會拋出異常。
使用延遲初始化屬性,是因為有些情況下,在聲明屬性的時候不能確定該屬性的初始化值,但是在後續的程序中,一定可以為其設置一個初始化值。lateinit修飾的屬性,需要程序員保證非空!
使用場景:在Android中,需要聲明頁麵的組件,在沒有findViewById時,需要設置一個null初始化值,如果用lateinit修飾,則可以不設置null。
在後麵會有一個延遲屬性,隻可以用val聲明,一般用於聲明一些初始化耗時的計算屬性,隻有在第一次訪問時計算並將計算結果保存為屬性值,再次訪問時,會直接使用保存的值,不會再次計算。示例代碼如下:
val lazyValue: String by lazy { println("computed!") "Hello" }
引申:聲明屬性時,默認初始化值在什麼情況下可以不設置?
l 屬性用abstract或lateinit修飾時。
l 聲明在接口中的屬性
l 擴展的屬性
l 屬性的所有自定義訪問器都沒有用過幕後字段field
7. 使用函數、匿名函數、lambda表達式給屬性賦值
class SetValue {
val funReturnUnit = returnUnit() //用無返回值的函數給屬性賦值
val funReturnValue = returnValue()//用有返回值的函數給屬性賦值
val lambda = { "lambda表達式" } //用lambda表達式給屬性賦值
val lambdaRun = { "運行的lambda表達式" }() //用lambda表達式執行結果給屬性賦值
val anonymousFun = fun(): String { //用匿名函數給屬性賦值
return "匿名函數"
}
val anonymousFunRun = fun(): String { //用匿名函數執行結果給屬性賦值
return "執行的匿名函數"
}()
fun returnValue(): String {
return "有返回值的函數"
}
fun returnUnit() {}
}
fun main(args: Array<String>) {
val impl = SetValue()
println("用無返回值的函數給屬性賦值: ${impl.funReturnUnit}")
println("用有返回值的函數給屬性賦值: ${impl.funReturnValue}")
println("用lambda表達式給屬性賦值: ${impl.lambda}")
println("用lambda表達式執行結果給屬性賦值: ${impl.lambdaRun}")
println("用匿名函數給屬性賦值: ${impl.anonymousFun}")
println("用匿名函數執行結果給屬性賦值: ${impl.anonymousFunRun}")
}
程序執行結果如下:
用無返回值的函數給屬性賦值: kotlin.Unit
用有返回值的函數給屬性賦值: 有返回值的函數
用lambda表達式給屬性賦值: () -> kotlin.String
用lambda表達式執行結果給屬性賦值: 運行的lambda表達式
用匿名函數給屬性賦值: () -> kotlin.String
用匿名函數執行結果給屬性賦值: 執行的匿名函數
三、 繼承
1. 繼承
繼承是強耦合的!
kotlin中所有類都隱式繼承自Any。類的默認修飾符是final,如果想要可以被繼承,那麼需要顯式聲明為open。
如果子類中不存在主構造函數時,可以在子類的次構造函數中使用super關鍵字初始化其基類型,調用父類的構造函數。
2. 子類調用父類的構造方法
子類的構造函數最終要調用父類構造函數。
l 當子類中存在主構造函數或者不存在任何構造函數時,需要在類頭初始化父類,父類後有括號。
l 當子類中隻存在次構造函數時,需要在次構造函數後麵用super調用父類的構造函數,這時,父類後無需括號。當父類可以使用無參方式調用構造函數初始化時,super關鍵字可以省略,這時,默認調用父類該構造函數。
區別:
Java中,構造函數沒有主次之分,所以調用任何一個父類構造函數都可以。
swift中,一個指定構造器必須調用直接父類的指定構造器,一個便利構造器隻能調用當前類的其他構造器,一個便利構造器必須最終調用一個指定構造器。
kotlin中,存在主構造函數時,與swift相同,當不存在主構造函數時,次構造函數可以調用父類中任何一個構造函數。
在初始化子類時,會按照當前類中構造方法的調用順序,反向依次執行。
完整的執行順序是:父類的主構造函數---->父類的次構造函數---->子類的主構造函數---->子類的次構造函數。
l 當子類不存在主構造函數時,次構造函數可以直接調用父類的構造函數。
l 當子類存在主構造函數時,隻能由主構造函數調用父類的構造函數。
3. 覆蓋函數
父類中方法默認修飾符是final,如果想要可以被子類重寫,需要顯式open,子類中重寫時,需要加override關鍵字,Java中不加關鍵字也不會報錯。
如果想要禁止再次被重寫,那麼需要顯式加上final。
構造函數不能被重寫。
4. 覆蓋屬性
與覆蓋函數類似,父類中聲明為open的屬性,可以在子類中使用override關鍵字重寫。
父類中的val屬性可以在子類中重寫為var,但是var屬性不可以被重寫為val的。
5. 覆蓋規則
父類與接口中,有相同名字的函數,必須要在子類中重寫,在重寫的方法中使用super<超類型名>.functionName()來聲明調用的是哪個超類型名中的函數。
當父類中與接口中同名的方法為final修飾時,在子類中無法重寫同名方法,會報錯。
當父類與接口有同名函數,並且接口中的函數沒有默認實現時:
a) 不重寫同名函數,會使用從父類中繼承來的函數,作為接口函數的實現。
b) 如果重寫,則可以使用super. functionName ()來調用父類函數。
kotlin中:如果接口同名函數有默認實現,那麼必須在子類重寫該方法,因為該子類繼承了較多的實現。在生產過程中需要盡量避免該現象的產生。
6. 抽象類
l 抽象類與類中的函數不需要open修飾,就可以被繼承。
l 可以用一個抽象成員覆蓋一個非抽象成員。
7. 伴生對象:關鍵字companion
l 可省略伴生對象的名稱,係統默認使用Companion作為伴生對象名稱
l 可以繼承類、實現接口
l 可以通過在伴生對象中創建外部類的實例來調用外部類的函數和屬性。
當伴生對象繼承了一個類的時候,那麼該類的所有成員屬性和成員方法,都可以用伴生對象外部類的點表達式靜態調用。
Q:取消static而引入伴生對象的意義:
A:把所有的靜態的屬性和方法都集中到了一起,代碼易讀性高。
引申:單例模式的實現
a) 伴生對象 val c = AA.instance
b) Object val c = C
四、 接口
1. 接口中的函數
kotlin中接口的函數可以有默認的實現。
l 有默認實現的函數可以不在實現類中重寫
l 有默認實現的函數,可以再實現類中的init塊中、次構造函數中、成員函數中、給屬性賦值時調用。
2. 接口中的屬性
它可以有屬性但必須聲明為抽象或提供訪問器實現。
l 抽象的屬性:需要在實現類中重寫
l 提供getter(可以加setter),可以用作計算屬性
3. 解決覆蓋衝突
與繼承的覆蓋規則相同,使用<超類型名>.functionName()。
五、 可見性修飾符
類、對象、接口、構造函數、方法、屬性和它們的 setter 都可以有 可見性修飾符。 (getter 總是與屬性有著相同的可見性。) 在 Kotlin 中有這四個可見性修飾符:private、 protected、 internal 和 public。 如果沒有顯式指定修飾符的話,默認可見性是 public。
l 如果你不指定任何可見性修飾符,默認為 public,這意味著你的聲明將隨處可見;
l private用於頂層聲明時,於當前文件內可見;用於類中聲明時,於當前類中可見;
l internal用於頂層聲明時,它會在相同模塊內隨處可見;用於類中聲明時,即使類的修飾符為public,用internal修飾的對象(成員屬性、成員函數等),在其它模塊也是不可見的;
l protected 本類與子類中可見,不適用於頂層聲明。
可見性修飾符作用域如下:
作用域 |
其他Module |
當前Module |
當前文件 |
本類 |
子類 |
public |
√ |
√ |
√ |
√ |
|
internal |
|
√ |
√ |
√ |
√ |
private |
|
|
√ |
√ |
|
protected |
|
|
|
√ |
√ |
六、 擴展
kotlin中能夠擴展一個類的新功能而無需繼承該類或使用像裝飾者這樣的任何類型的設計模式,(僅)支持擴展函數和屬性。
1. 擴展是靜態解析的
靜態解析:在編譯時,就已經確定類型。
動態解析:在編譯時不確定類型,在執行時才會確認到底是什麼類型(比如:多態中的繼承,父類中定義抽象方法,每個子類有不同的實現)。
按照調用的擴展函數所定義的接收參數類型決定,該調用哪一個類的擴展(父子類)。
2. 擴展函數
成員函數:被擴展的類原有的函數
l 當擴展函數與成員函數同名同參時,調用函數,執行成員函數。
l 擴展函數可以重載成員函數。
l 可以給子類和父類擴展相同名稱的函數。
l 不可以擴展構造函數,擴展構造函數會改變原有的類。
l 擴展函數需要有函數體。
3. 擴展屬性
由於擴展沒有實際的將成員插入類中,因此對擴展屬性來說幕後字段是無效的。這就是為什麼擴展屬性不能有初始化器。他們的行為隻能由顯式提供的 getters/setters 定義(隻能擴展計算屬性)。
不能擴展類中已有的屬性。
4. 其他
對伴生對象、內部類、嵌套類、接口以及Java中定義的類和接口進行擴展。
l 對伴生對象、內部類、嵌套類的擴展方式:外部類.擴展類.方法或屬性,伴生對象如果省略了類名,則用Companion代替。
l 對接口擴展,與普通擴展相同,擴展函數需要有函數體。
l 對java中定義的類與接口的擴展與在kotlin中相同。
5. 擴展的作用域
l 要使用所定義包之外的一個擴展,我們需要在調用方導入。
6. 擴展聲明為成員
分發接收者:如果一個類內部有其他類的擴展,那麼它的實例就是分發接收者。
擴展接收者:被擴展的類的實例。
注意:
a) 在頂層定義的擴展,可以在任意可以見到當前擴展的地方使用(可見性修飾符修飾)。
b) 在一個類中給其他類進行擴展,那麼這些擴展,隻能在當前類或者當前類的子類中使用,不可以在類外部使用。
Q1:給一個類擴展兩個相同名稱的函數會怎樣?
a) 如果在頂層擴展,將會報錯,不可以有這樣的寫法。
b) 如果擴展聲明為成員,在父類與子類中分別擴展,可以override。
Q2:擴展一個與父類成員函數相同的函數會怎樣?
無效果,因為子類繼承了父類的成員函數,調用時會執行子類中的成員函數。
七、 數據類
在java中的Model類是把有關係的一組數據封裝在一起的類文件,通常Model類中會隻定義屬性和get、set方法,而且每個Model類都是一個單獨的文件。kotlin中對此進行了優化,將這些隻保存數據的類定義為數據類並用data關鍵字修飾,一個文件中可以聲明多個數據類。
編譯器自動從主構造函數中聲明的所有屬性導出以下成員:
l equals()/hashCode() 對,
l toString() 格式是 "User(name=John, age=42)",
l componentN()函數 按聲明順序對應於所有屬性(多用於解構聲明),
l copy() 函數,複製出克隆對象,可以在複製的時候,對保存的屬性進行修改
數據類可繼承類與實現接口。
可以在數據類中定義自己的方法,可擴展。
八、 密封類
相當於枚舉類型的擴展,但可以比枚舉類型做更多的工作。
密封類的子類隻能與密封類定義在同一個文件內,但繼承密封類子類的類(間接繼承者)可以放在任何位置,不僅限於同文件內。
密封類不能實例化,但密封類的子類可以。
使用密封類的關鍵好處在於使用 when 表達式 的時候,如果能夠驗證語句覆蓋了所有情況,就不需要為該語句再添加一個 else 子句了
最後更新:2017-07-14 09:02:55