深入構(gòu)造器
構(gòu)造器用于在創(chuàng)建實例時執(zhí)行初始化。構(gòu)造器是創(chuàng)建對象的重要途徑(即使使用工廠模式、反射等方式創(chuàng)建對象,其實質(zhì)也依然是依賴于構(gòu)造器),因此 Kotlin類必須包含一個或一個以上的構(gòu)造器。
主構(gòu)造器和初始化塊
前面己經(jīng)提到, Kotlin 類可以定義0~1個主構(gòu)造器和0~N 個次構(gòu)造器。如果主構(gòu)造器沒有任何注解或可見性修飾符,則可以省略 constructor關(guān)鍵字。
主構(gòu)造器作為類頭的一部分,可以聲明形參,但它自己并沒有執(zhí)行體。那么主構(gòu)造器的形參有什么用呢 ?其作用主要有兩點。
- 初始化塊可以使用主構(gòu)造器定義的形參。
- 在聲明屬性時可以使用主構(gòu)造器定義的形參 。
由此可見, Kotlin 的主構(gòu)造器并不是傳統(tǒng)意義上的構(gòu)造器,它更像 Java 的初始化塊,或者說是對初始化塊的增強,Java的初始化塊不能傳入?yún)?shù): Kotlin通過主構(gòu)造器的設(shè)計,允許為初始化塊傳入?yún)?shù)。
初始化塊的語法格式如下:
init {
//初始化塊中的可執(zhí)行代碼,可以使用主構(gòu)造器定義的參數(shù)
}
初始化塊中的代碼可以使用主構(gòu)造器定義的參數(shù),也可以包含任何可執(zhí)行語句,包括定義局部變量、調(diào)用其他對象的方法,以及使用分支、循環(huán)語句等。如下所示:
class Person(name:String){
//下面定義一個初始化塊
init {
var a =6
if(a>4){
println ("Person初始化塊:局部變量a的值大于4")
}
println("Person的初始化塊")
println("name參數(shù)為:${ name}")
}
//定義第二個初始化塊
init {
println("Person 的第二個初始化塊")
}
}
fun main(args: Array<String>) {
Person("sy")
//輸出:Person初始化塊:局部變量a的值大于4
//Person的初始化塊
//name參數(shù)為:sy
//Person 的第二個初始化塊
}
從上面可以看出來當程序通過主構(gòu)造器創(chuàng)建對象時,系統(tǒng)其實就是調(diào)用該類里定義的初始化塊,如果一個類里定義了兩個普通初始化塊,則前面定義的初始化塊先執(zhí)行,后面定義的初始化塊后執(zhí)行。
Kotlin中允許一個類里定義兩個初始化塊,但這沒有任何意義。因為初始化塊是在創(chuàng)建對象時隱式執(zhí)行的,而且它們總是全部執(zhí)行,因此完全可以把多個普通初始化塊合并成一個初始化塊,從而讓程序更加簡潔,可讀性更強。
從上面結(jié)果不難看出,程序調(diào)用主構(gòu)造器創(chuàng)建對象,實際上就是執(zhí)行初始化塊。由此可見,主構(gòu)造器的主要作用就是為初始化塊定義參數(shù),因此主構(gòu)造器更像是初始化塊的一部分。也可以說,初始化塊就是主構(gòu)造器的執(zhí)行體 。
構(gòu)造器最大的用處就是在創(chuàng)建對象時執(zhí)行初始化,但由于初始化塊就是主構(gòu)造器的執(zhí)行體, 因此,如果希望為對象的屬性顯式指定初始值,則也可以通過初始化塊來指定。如果程序員沒有為 Kotlin類提供任何構(gòu)造器(主構(gòu)造器和次構(gòu)造器),則系統(tǒng)會為這個類提供一個無參數(shù)的主構(gòu)造器,這個構(gòu)造器的執(zhí)行體為空,不做任何事情。
//提供主構(gòu)造器,該構(gòu)造器包含兩個參數(shù)
class ConstructorTest(name:String,count:Int){
var name:String
var count:Int
//定義初始化塊,它相當于主構(gòu)造器的執(zhí)行體
init {
//初始化塊中的 this 代表其進行初始化的對象
//下面兩行代碼將傳入的兩個參數(shù)賦給 this 所代表的對象的 name 和 count 屬性
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
//使用向定義的構(gòu)造器來創(chuàng)建對象
//系統(tǒng)將會對該對象執(zhí)行自定義的初始化
var tc =ConstructorTest("cc",28)
//輸出ConstructorTest 對象的 name屬性
println(tc.name)
}
次構(gòu)造器和構(gòu)造器重載
Kotlin 允許使用 constructor 關(guān)鍵字定義 N 個次構(gòu)造器,而且Kotlin的主構(gòu)造器其實屬于初始化塊(或者說,初始化塊其實是主構(gòu)造器的執(zhí)行體),因此 Kotlin 要求所有的次構(gòu)造器必須委托調(diào)用主構(gòu)造器,可以直接委托或通過別的次構(gòu)造器間接委托。所謂“委托”,其實就是要先調(diào)用主構(gòu)造器(執(zhí)行初始化塊中的代碼),然后才執(zhí)行次構(gòu)造器代碼。
如果兩個構(gòu)造器中有相同的初始化代碼,就可以把它們放在初始化塊中定義。如果這些初始化代碼需要參數(shù),則可將參數(shù)放在主構(gòu)造器中定義 。通過把多個構(gòu)造器中的相同代碼提取到初始塊中定義,能更好地提高初始化代碼的復用性,提高整個應用的可維護性。
同一個類里具有多個構(gòu)造器 , 多個構(gòu)造器的形參列表不同,即被稱為構(gòu)造器重載。程序可通過不同的構(gòu)造器來創(chuàng)建對象,但不管使用哪個構(gòu)造器,首先都要先調(diào)用主構(gòu)造器(執(zhí)行初始化塊代碼)。
下面程序示范了構(gòu)造器重載,利用構(gòu)造器重載就可以通過不同的構(gòu)造器來創(chuàng)建對象。
class ConstructorOverload {
var name: String?
var count: Int
init {
println("初始化塊!")
}
//提供無參數(shù)的構(gòu)造器
constructor() {
name = null
count = 0
}
constructor(name:String,count:Int){
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
//通過無參數(shù)的構(gòu)造器創(chuàng)建 ConstructorOverload 對象
var oc1 = ConstructorOverload()
//通過有參數(shù)的構(gòu)造器創(chuàng)建 ConstructorOverload 對象
var oc2 = ConstructorOverload("xq",111)
//輸出初始化塊!
//初始化塊!
//xq 111
println(oc2.name + " " + oc2.count)
}
從上面的運行結(jié)果可以看出,不管調(diào)用哪個構(gòu)造器創(chuàng)建對象,系統(tǒng)總會先執(zhí)行初始化塊,也就是說,初始化塊總會在所有次構(gòu)造器之前執(zhí)行。用Kotlin的專業(yè)術(shù)語來說,叫作 : 所有的次構(gòu)造器都要委托調(diào)用初始化塊。
上面的 ConstrctorOverload沒有定義主構(gòu)造器,因此次構(gòu)造器不需要委托主構(gòu)造器。下面再定義一個帶主構(gòu)造器的類 。
class User1(name: String) {
var name: String
var age: Int
var info: String? = null
init {
println("User 的初始化塊”")
this.name = name
this.age = 0
}
//委托給主構(gòu)造器
constructor(name: String, age: Int) : this(name)
//委托給( String, Int)構(gòu)造器
constructor(name: String, age: Int, info: String) : this(name, age) {
this.info = info
}
}
fun main(args: Array<String>) {
//調(diào)用主構(gòu)造器
var user1 = User1("xq")
println("${user1.name} => ${user1.age} => ${user1.info}")
//調(diào)用( String, Int)構(gòu)造器
var user2 = User1("cq",21)
println("${user2.name} => ${user2.age} => ${user2.info}")
//調(diào)用( String, Int, String)構(gòu)造器
var user3 = User1("xy",20,"tsss")
println("${user3.name} => ${user3.age} => ${user3.info}")
}
從上面代碼可以看出, Kotiin 使用“:this(參數(shù))”的語法委托另 一個構(gòu)造器,到底委托哪個構(gòu)造器則取決于傳入的參數(shù)。系統(tǒng)會根據(jù)傳入的參數(shù)來推斷委托了哪個構(gòu)造器。
主構(gòu)造器聲明屬性
Kotlin允許在主構(gòu)造器上聲明屬性,直接在參數(shù)之前使用var或val即可聲明屬性,使用 var聲明的是讀寫屬性,使用 val聲明的是只讀屬性。當程序調(diào)用這種方式聲明的主構(gòu)造器創(chuàng)建對象時,傳給該構(gòu)造器的參數(shù)將會賦值給對象的屬性。
例如如下程序:
class Item2(var name:String,val num:Int){
}
fun main(args: Array<String>) {
var item2 = Item2("xq",1)
println(item2.name + " " + item2.num)
}
如果主構(gòu)造器的所有參數(shù)都有默認值,程序能以構(gòu)造參數(shù)的默認值來調(diào)用該構(gòu)造器(即不需要為構(gòu)造參數(shù)傳入值),此時看上去就像調(diào)用無參數(shù)的構(gòu)造器。
class Item2(var name:String= "xq",val num:Int = 0){
}
fun main(args: Array<String>) {
var item = Item2()
println(item.name + " " + item.num)
var item2 = Item2("xqs",2)
println(item2.name + " " + item2.num)
}
類的繼承
繼承是面向?qū)ο蟮娜筇卣髦唬彩菍崿F(xiàn)軟件復用的重要手段。 Kotlin的繼承同樣是單繼承:每個子類最多只有一個直接父類。
繼承的語法
Kotiin 的子類繼承父類的語法格式如下:
修飾符 class SubClass : Superclass{
//類定義部分
}
從上面的語法格式來看,定義子類的語法非常簡單 ,只需在原來的類定義中添加SuperClass,即可表明該子類繼承了 SuperClass類。
從子類的角度來看, 子類擴展了父類; 但從父類的角度來看,父類派生出子類。也就是說,擴展和派生所描述的是同一個動作,只是觀察的角度不同而己。 如果在定義一個 Kotlin類時并未顯式指定這個類的直接父類,則這個類默認擴展 Any類 。因此Any類是所有類的父類,要么是其直接父類,要么是其間接父類 。需要說明的是 ,Any類不是 java.lang.Object類 ,Any類只有 equals()、hashCode()和 toString()。這 3個方法。
還有一點需要說明的是, Kotlin的類默認就有 final修飾,因此Kotlin的類默認是不能派生子類的。 為了讓一個類能派生子類,需要使用open修飾該類。
open 就是 final 的反義詞,用于取消 Kotlin 自動添加的 final修飾符 。
下面我們嘗試定義如下父子類 。
open class BaseClass {
}
class SubClass : BaseClass {
}
如果嘗試編譯上面兩個類 , 就會發(fā)現(xiàn)程序無法通過編譯,這是為什么呢?
java子類構(gòu)造器總要調(diào)用父類構(gòu)造器一次,子類構(gòu)造器調(diào)用父類構(gòu)造器分如下幾種情況:
- 子類構(gòu)造器執(zhí)行體的第一行使用 super(參數(shù))顯式調(diào)用父類構(gòu)造器,系統(tǒng)將根據(jù) super(參數(shù))調(diào)用中傳入的實參列表調(diào)用父類對應的構(gòu)造器 。
- 子類構(gòu)造器執(zhí)行體的第一行代碼使用 this(參數(shù))顯式調(diào)用本類中重載的構(gòu)造器,系統(tǒng)將根據(jù) this(參數(shù))調(diào)用中傳入的實參列表調(diào)用本類中的另一個構(gòu)造器。 調(diào)用本類中的另一個構(gòu)造器最終還是要調(diào)用父類構(gòu)造器。
- 子類構(gòu)造器執(zhí)行體中既沒有super(參數(shù))調(diào)用,也沒有 this(參數(shù))調(diào)用,系統(tǒng)將會在執(zhí)行子類構(gòu)造器之前 , 隱式調(diào)父類無參數(shù)的構(gòu)造器 。
Kotlin的構(gòu)造器同樣要遵守這個規(guī)則,只不過 Kotlin為之換了個新說法 :委托父類構(gòu)造器 。而且由于 Kotlin 的構(gòu)造器分為主構(gòu)造器和次構(gòu)造器,因此情況略微復雜一些。 下面分主構(gòu)造器和次構(gòu)造器進行詳細說明 。
1. 子類的主構(gòu)造器
如果子類定義了主構(gòu)造器,由于主構(gòu)造器屬于類頭部分(如果不定義初始化塊,它就沒有執(zhí)行體),為了讓主構(gòu)造器能調(diào)用父類構(gòu)造器,因此主構(gòu)造器必須在繼承父類的同時委托調(diào)用父類構(gòu)造器 。 例如如下代碼 :
open class BaseClass {
var name:String
constructor(name:String){
this.name = name
}
}
//子類沒有顯式聲明主構(gòu)造器
//子類默認有一個主構(gòu)造器,因此要在聲明繼承時委托調(diào)用父類構(gòu)造器
class SubClass1 : BaseClass("foo1") {
}
//子類顯式聲明主構(gòu)造器
//主構(gòu)造樓必須在聲明繼承時委托調(diào)用父類構(gòu)造器
class SubClass2(name: String):BaseClass(name){
}
其中 SubClass1沒有顯式聲明主構(gòu)造器,系統(tǒng)會為該類自動生成一個無參數(shù)的主構(gòu)造器,因此程序在繼承 BaseClass時必須立即調(diào)用父類構(gòu)造器
為 SubClass2 顯式定義了一個帶參數(shù)的主構(gòu)造器,因此程序同樣需要在繼承 BaseClass時必須立即調(diào)用父類構(gòu)造器。
2. 子類的次構(gòu)造器
次構(gòu)造器同樣需要委托調(diào)用父類構(gòu)造器。
如果子類定義了主構(gòu)造器,由于子類的次構(gòu)造器總會委托調(diào)用子類的主構(gòu)造器(直接或間接),而主構(gòu)造器一定會委托調(diào)用父類構(gòu)造器,因此子類的所有次構(gòu)造器最終也調(diào)用了父類構(gòu)造器。
如果子類沒有定義主構(gòu)造器,則此時次構(gòu)造器委托調(diào)用父類構(gòu)造器可分為 3 種方式。
- 子類構(gòu)造器顯式使用:this(參數(shù))顯式調(diào)用本類中重載的構(gòu)造器,系統(tǒng)將根據(jù) this(參數(shù))調(diào)用中傳入的實參列表調(diào)用本類中的另一個構(gòu)造器。調(diào)用本類中的另一個構(gòu)造器最終還是要調(diào)用父類構(gòu)造器。
- 子類構(gòu)造器顯式使用:super(參數(shù))委托調(diào)用父類構(gòu)造器,系統(tǒng)將根據(jù) super(參數(shù))調(diào)用中傳入的實參列表調(diào)用父類對應的構(gòu)造器。
- 子類構(gòu)造器既沒有super(參數(shù))調(diào)用,也沒有this(參數(shù))調(diào)用,系統(tǒng)將會在執(zhí)行子類構(gòu)造器之前,隱式調(diào)用父類無參數(shù)的構(gòu)造器。
這段描述和Java 子類構(gòu)造器調(diào)用父類構(gòu)造器的述相似嗎?相似就對了。正如前面所介紹的, Kotiin 的次構(gòu)造器相當于 Java 的構(gòu)造器,因此 Kotlin 的次構(gòu)造器委托調(diào)用父類構(gòu)造器的 3 種方式,正好對應于 Java 構(gòu)造器調(diào)用父類構(gòu)造器的 3 種方式。它們唯一的區(qū)別只是寫法不同 : Java 將調(diào)用父類構(gòu)造器寫在方法體內(nèi), Kotlin 將委托父類構(gòu)造器寫在構(gòu)造器聲明中。
如下代碼示范了沒有主構(gòu)造器的子類的次構(gòu)造器是如何調(diào)用父類構(gòu)造器的
open class BaseClass {
constructor() {
println("Base 的無參數(shù)的構(gòu)造器")
}
constructor(name: String) {
println("Base 的帶一個 String參數(shù):${name}的構(gòu)造器")
}
}
class Sub : BaseClass {
//構(gòu)造器沒有顯式委托
//因此該次構(gòu)造器將會隱式委托調(diào)用父類無參數(shù)的構(gòu)造器
constructor() {
println("Sub 的無參數(shù)的構(gòu)造器")
}
//構(gòu)造器用 super(name)顯式委托父類帶 String 參數(shù)的構(gòu)造器
constructor(name: String) : super(name) {
println("Sub 的 String 構(gòu)造器, String 參數(shù)為:${name}")
}
//構(gòu)造器用 this(name)顯式委托本類中帶 String參數(shù)的構(gòu)造器
constructor(name: String, age: Int) : this(name) {
println("Sub 的 String Int構(gòu)造器, Int 參數(shù)為:${age}")
}
}
fun main(args: Array<String>) {
Sub()
Sub("Sub")
Sub("子類",12)
}
上面的 Sub 類沒有定義主構(gòu)造器,類體中 3 行粗體字代碼定義了3個次構(gòu)造器 。
- 第一個次構(gòu)造器既沒有 super(參數(shù))委托 ,也沒有(this)委托,因此該構(gòu)造器會隱式委托調(diào)用父類無參數(shù)的構(gòu)造器 。
- 第二個次構(gòu)造器使用:super(name)委托調(diào)用, 其中 name是一個 String類型的參數(shù),因此該構(gòu)造器屬于顯式委托調(diào)用父類帶一個 String參數(shù)的構(gòu)造器。
- 第三個次構(gòu)造器使用:this(name)委托調(diào)用,其中 name是一個 String類型的參數(shù),因此該構(gòu)造器屬于顯式委托調(diào)用該類中帶一個 String參數(shù)的構(gòu)造器,即調(diào)用前一個構(gòu)造器:而前一個構(gòu)造器委托調(diào)用了父類構(gòu)造器, 因此該次構(gòu)造器最終也調(diào)用了父類構(gòu)造器一次。
當調(diào)用子類構(gòu)造器來初始化子類對象時,父類構(gòu)造器總會在子類構(gòu)造器之前執(zhí)行;不僅如此,在執(zhí)行父類構(gòu)造器時,系統(tǒng)會再次上溯執(zhí)行其父類構(gòu)造器 ...... 依此類推,創(chuàng)建任何 Kotlin對象,最先執(zhí)行的總是 Any 類的構(gòu)造器 。
重寫父類方法
子類繼承父類,將可以獲得父類的全部屬性和方法 。
open class Fruit(var weight: Double) {
fun info() {
println("我是一個水果,重${weight}克")
}
}
class Apple : Fruit(0.0)
fun main(args: Array<String>) {
//創(chuàng)建Apple對象
var apple = Apple()
// Apple 對象本身沒有 weight 屬性
//因為 Apple 的父類有 weight 屬性,所以也可以訪問 Apple 對象的 weight 屬性
apple.weight = 5.6
//調(diào)用 Apple 對象的 info ()方法
apple.info()
}
上面的 Apple類只是一個空類,但程序中創(chuàng)建了Apple對象之后,可以訪問該 Apple對象的weight屬性和info()方法,這表明 Apple對象也具有了weight屬性和info()方法,這就是繼承的作用。
子類繼承了父類,子類是一個特殊的父類。大部分時候子類總是以父類為基礎(chǔ)額外增加新的屬性和方法。但有一種情況例外 : 子類需要重寫父類的方法。例如鳥類都包含了飛翔方法,其中駝鳥是一種特殊的鳥類,因此駝鳥應該是鳥的子類, 它也將從鳥類獲得飛翔方法, 但這個飛翔方法明顯不適合駝鳥,為此 ,駝鳥需要重寫鳥類的方法。
open class Bird {
open fun fly() {
println("我在天空里自由自在地飛翔...")
}
}
class Ostrich:Bird(){
override fun fly() {
println("我只能在地上奔跑...")
}
}
fun main(args: Array<String>) {
var os =Ostrich()
//執(zhí)行Ostrich對象的 fly()方法,將輸出”我只能在地上奔跑...”
os.fly()
}
留意上面的 fly()方法,該方法同樣使用了 open 修飾符,Kotlin 默認為所有方法添加 final修飾符,阻止該方法被重寫,添加 open 關(guān)鍵字用于阻止 Kotlin 自動添加 final 修飾符。
kotlin類重寫父類的方法必須添加 override修飾符, 就像 Java 的@Override 注解,只不過 Java 的@Override是可選的 , 而 Kotlin的override 修飾符是強制的。
這種子類包含與父類同名方法的現(xiàn)象被稱為方法重寫( Override),也被稱為方法覆蓋 。可以說子類重寫了父類的方法,也可以說子類覆蓋了父類的方法 。 方法的重寫要遵循“兩同兩小一大”規(guī)則,“兩同”即方法名相同、形參列表相同;“兩小”指的是子類方法的返回值類型應比父類方法的返回值類型更小或相等,子類方法聲明拋出的異常類應比父類方法聲明拋出的異常類更小或相等;“一大”指的是子類方法的訪問權(quán)限應比父類方法的訪問權(quán)限更大或相等。
當子類覆蓋了父類方法后,子類的對象將無法訪問父類中被覆蓋的方法,但可以在子類方法中調(diào)用父類中被覆蓋的方法。如果需要在子類方法中調(diào)用父類中被覆蓋的方法,則可以使用 super作為調(diào)用者來調(diào)用父類中被覆蓋的方法 。
如果父類方法具有 private 訪問權(quán)限,則該方法對其子類是隱藏的,因此其子類無法訪問該方法,也就是無法重寫該方法。如果子類中定義了一個與父類 private 方法具有相同的方法名、相同的形參列表、相同的返回值類型的方法,那么這不是重寫,只是在子類中重新定義了 一個新方法 。
重寫父類的屬性
重寫父類的屬性與重寫父類的方法大致相似:父類被重寫的屬性必須使用 open 修飾,子類重寫的屬性必須使用 override修飾。此外,屬性重寫還有如下兩個限制:
- 重寫的子類屬性的類型與父類屬性的類型要兼容。
- 重寫的子類屬性要提供更大的訪問權(quán)限 。此處包含兩方面的含義 : 1.在訪問權(quán)限方面, 子類屬性的訪問權(quán)限應比父類屬性的訪問權(quán)限更大或相等。2.只讀屬性可被讀寫屬性重寫,但讀寫屬性不能被只讀屬性重寫。
open class Item {
open protected var price: Double = 5.9
open val name: String = ""
open var validDays: Int = 0
}
class Book : Item {
//正確重寫了父類屬性,類型兼容,訪問權(quán)限更大
override public var price: Double
//正確重寫了父類屬性,讀寫屬性重寫只讀屬性
override var name = "圖書"
//重寫錯誤 ,只讀屬性不能重寫讀寫屬性
//override val validDays :Int =2
constructor() {
price = 3.1
}
}
super 限定
如果需要在子類方法中調(diào)用父類中被覆蓋的法或?qū)傩裕瑒t可使用 super限定。例如,為上面的 Ostrich 類添加一個方法,在這個方法中調(diào)用 Bird類中被覆蓋的的()方法。
fun callOverridedMethod() {
//在子類方法中通過 super 顯式調(diào)用父類中被覆蓋 的方法
super.fly()
}
借助 callOverrided.Method()方法,就可以讓 Ostrich對象既可以調(diào)用自己重寫的 fly()方法, 也可以調(diào)用 Bird類中被覆蓋的 fly()方法(調(diào)用 callOverridedMethod()方法即可)。
super是 Kotlin提供的一個關(guān)鍵字,用于限定該對象調(diào)用它從父類繼承得到的屬性或方法。
如果子類重寫了父類的屬性,那么子類中定義的方法直接訪問該屬性默認會訪問到子類中定義的屬性,無法訪問到父類中被重寫的屬性。在子類定義的方法中可以通過 super來訪問父類中被重寫的屬性。
open class BaseClass1 {
open var a: Int = 5
}
class Subclass : BaseClass1() {
override var a: Int = 7
fun accessOwner() {
println(a)
}
fun accessBase() {
//通過 super 限定訪問從父類繼承得到的 a 屬性
println(super.a)
}
}
強制重寫
如果子類從多個直接超類型(接口或類)繼承了同名的成員,那么 Kotlin 要求子類必須重寫該成員。如果需要在子類中使用super來引用超類型中的成員,則可使用尖括號超類型名限定的 super進行引用,如 super<Bar>。
open class Foo{
open fun test(){
println("Foo的test()")
}
fun foo(){
println("foo")
}
}
interface Bar{
//接口中成員默認是 open 的
fun test(){
println("Bar的test")
}
fun bar(){
println("bar")
}
}
class Wow:Foo(),Bar{
//編譯器要求必須重寫 test()
override fun test() {
//調(diào)用父接口 Bar 的 test()
super<Bar>.test()
//調(diào)用父類 Foo的 test()
super<Foo>.test()
}
}
fun main(args: Array<String>) {
var w = Wow()
w.test()
}
上面程序在 Foo類中定義了一個可被重寫的 test()方法,在 Bar接口中也定義了一個可被重寫的 test()方法(接口中的方法默認有 open修飾符)。
當子類 Wow 同時繼承 Foo、 Bar時, 它會獲得兩個直接父類中定義的 test()方法,此時編譯器要求子類 Wow 必須重寫 test()方法,如上面程序的 Wow 類中的 test()方法所示。不僅如此, 如果程序希望在子類中調(diào)用父類 Foo 的 test()方法,則可通過 super<Foo>.test()進行調(diào)用:如果希望在子類中調(diào)用父接口 Bar 的 test()方法,則可通過 super<Bar>.test()進行調(diào)用。
多態(tài)
與Java類似, kotlin的變量也有兩個類型: 一個是編譯時類型,一個是運行時類型。編譯時類型由聲明該變量時使用的類型決定,運行時類型由實際賦給該變量的對象決定。如果編譯時類型和運行時類型不一致,就可能出現(xiàn)所謂的多態(tài)( Polymorphism)。
多態(tài)性
open class BaseClass2 {
open var book = 6
fun base() {
println("父類的普通方法 ")
}
open fun test() {
println("父類的被覆蓋的方法")
}
}
class SubClass : BaseClass2() {
//重寫父類的屬性
override var book = 60
override fun test() {
println("子類的覆蓋父類的方法")
}
fun sub() {
println("”子類的普通方法")
}
}
fun main(args: Array<String>) {
//下面編譯時類型和運行時類型完全一樣,因此不存在多態(tài)
var bc:BaseClass2 = BaseClass2()
//輸出 6
println(bc.book)
//下面兩次調(diào)用將執(zhí)行 BaseClass 的方法
bc.test()
bc.base()
//下面編譯時類型和運行時類型完全一樣,因此不存在多態(tài)
var sub:SubClass = SubClass()
//輸出 60
println(sub.book)
//下面調(diào)用將執(zhí)行從父類繼承的 base ()方法
sub.base()
//下面調(diào)用將執(zhí)行當前類的 test ()方法
sub.test()
//下面編譯時類型和運行時類型不一樣,多態(tài)發(fā)生
var ploymophicBc:BaseClass2 = SubClass()
//輸出 60 一一表明訪問的依然是子類對象的屬性
println(ploymophicBc.book)
//下面調(diào)用將執(zhí)行從父類繼承的 base ()方法
ploymophicBc.base()
//下面調(diào)用將執(zhí)行當前類的 test ()方法
ploymophicBc.test()
//因為 ploymophicBc 的編譯時類型是 BaseClass
//BaseClass 類沒有提供 sub ()方法,所以下面代碼編譯時會出現(xiàn)錯誤
//ploymophicBc.sub()
}
上面程序的 main()函數(shù)中顯式創(chuàng)建了3個變量,對于前兩個變量 bc 和sub,它們的編譯時類型和運行時類型完全相同,因此調(diào)用它們的屬性和方法沒有任何問題。但第三個變量 ploymophicBc則比較特殊,它的編譯時類型是 BaseClass,而運行時類型是 SubClass, 當調(diào)用該變量的 test()方法(BaseClass類中定義了該方法,子類 Subclass覆蓋了父類的該方法) 時,實際執(zhí)行的是 SubClass類中覆蓋后的 test()方法,這就可能出現(xiàn)多態(tài)。
因為子類其實是一種特殊的父類,因此 Kotlin 允許把一個子類對象直接賦給一個父類變量,無須任何類型轉(zhuǎn)換,或者被稱為向上轉(zhuǎn)型( upcasting),向上轉(zhuǎn)型由系統(tǒng)自動完成。
當把一個子類對象直接賦給父類變量時,例如上面的 var ploymophicBc: BaseCalss2 = new Subclass(),這個 ploymophicBc 變量的編譯時類型是 BaseClass,而運行時類型是 Subclass,當運行時調(diào)用該變量的方法時,其方法行為總是表現(xiàn)出子類方法的行為特征,而不是父類方法的行為特征,這就可能出現(xiàn):相同類型的變量,調(diào)用同一個方法時呈現(xiàn)出多種不同子類的行為特征,這就是多態(tài)。
上面的 main()函數(shù)中注釋掉了 ploymophicBc.sub(),這行代碼會在編譯時引發(fā)錯誤。雖然ploymophicBc 引用的對象實際上確實包含 sub()方法(例如,可以通過反射來執(zhí)行該方法),但
因為它的編譯時類型為 BaseClass, 因此編譯時無法調(diào)用 sub()方法。 與方法類似的是,對象的屬性同樣具有多態(tài)性。比如上面通過 ploymophicBc輸出它的 book屬性變量時,依然是輸出 Subclass類中定義的屬性,而不是輸出 BaseClass父類中定義的屬性。
使用 is檢查類型
變量只能調(diào)用其編譯時類型的方法,而不能調(diào)用其運行時類型的方法,即使它實際所引用的對象確實包含該方法。如果要讓這個變量調(diào)用其運行時類型的方法,就需要把它強制轉(zhuǎn)換成運行時類型,強制轉(zhuǎn)換需要借助于強制轉(zhuǎn)型運算符。 Kotlin的類型轉(zhuǎn)換運算符包含 as和 as?兩個。
由于向上轉(zhuǎn)型可由 Kotlin 自動轉(zhuǎn)換,因此強制轉(zhuǎn)型通常總是向下轉(zhuǎn)型。為了保證類型轉(zhuǎn)換不會出錯,Kotlin 提供了類型檢查運算符: is 和!is。
is 運算符的前一個操作數(shù)通常是一個變量,后一個操作數(shù)通常是一個類(也可以是接口,可以把接口理解成一種特殊的類),它用于判斷前面的變量是否引用后面的類,或者其子類、 實現(xiàn)類的實例。如果是,則返回 true,否則返回 false。
此外, Kotlin的 is和!is都非常智能,當程序使用 is或!is判斷之后,只要 Kotlin能推斷出變量屬于某種類型, Kotlin就 會自動將該變量的編譯時類型轉(zhuǎn)換為指定類型。
import java.util.*
fun main(args: Array<String>) {
//聲明 hello 時使用 Any 類,則 hello 的編譯時類型是 Any
//hello變量的實際類型是 String
var hello :Any = "Hello"
println("字符串是否是String類的實例:${hello is String}")
//Date與Any類存在繼承關(guān)系, 可以進行is運算
println("”字符串是否是 Date 類的實例 :${hello is Date}")
//由于 hello 的編譯時類型是 Any,因此編譯時它沒有 length 屬性
//println(hello.length)
//先判斷 hello 為 String, 在 if 條件體中 hello 被自動轉(zhuǎn)換為 String 類型
if(hello is String){
println(hello.length)
}
}
使用 as 運算符轉(zhuǎn)型
除使用 is 自動轉(zhuǎn)型之外, Kotlin 也支持使用 as 運算符進行強制轉(zhuǎn)型 。 Kotlin 提供了兩個向下轉(zhuǎn)型運算符。
as:不安全的強制轉(zhuǎn)型運算符,如果轉(zhuǎn)型失敗,程序?qū)l(fā) ClassCastException 異常
as?: 安全的強制轉(zhuǎn)型運算符,如果轉(zhuǎn)型失敗,程序不會引發(fā)異常,而是返回 null。
強制向下轉(zhuǎn)型只能在具有繼承關(guān)系的兩種類型之間進行,如果是兩種沒有任何繼承關(guān)系的類型 ,則無法進行類型轉(zhuǎn)換,編譯時就會發(fā)出警告:轉(zhuǎn)換不可能成功。如果試圖把一個父類變量轉(zhuǎn)換成子類類型,則該變量實際引用的實例必須是子類實例才行(即編譯時類型為父類類型, 而運行時類是子類類型),否則就會轉(zhuǎn)換失敗。
fun main(args: Array<String>) {
val obj: Any = "Hello"
//obj 變量的編譯時類理為 Any, Any 與 String 存在繼承關(guān)系,可以進行轉(zhuǎn)換
//而且 obj 實際引用的實例是 String 類型,所以運行時也可通過
val objString: String = obj as String
println(objString)
//定義一個 objPri 變量,編譯時類型為 Any,實際類型為 Int
val objPri: Any = 5
//objPri 變量的編譯時類型為 Any, objPri的運行時類型為 Int
//Any 與 String 存在繼承關(guān)系,可以進行轉(zhuǎn)換,編譯通過
//但 objPri 變量實際引用的實例是 Int 類型 , 所以轉(zhuǎn)換失敗
val str: String = objPri as String //轉(zhuǎn)換失敗,引發(fā) ClassCastException
}