kotlin多態(tài)

多態(tài)定義

多態(tài)是指允許不同類(lèi)的對(duì)象對(duì)同一消息做出相應(yīng),即對(duì)同一消息可以根據(jù)發(fā)送對(duì)象的不同而采用不同的行為方式。(發(fā)送消息就是函數(shù)調(diào)用)

open class Father{
    open fun bar(){
        println("father.bar")
    }
}
open class Son:Father(){
    override fun bar() {
        println("son.bar")
    }
}
class GrandSon:Son(){
    override fun bar() {
        println("grandSon.bar")
    }
}

class Play{
    fun play(father: Father){
        father.bar()
    }
}
fun main() {
    val play = Play()
    play.play(Father())
    play.play(Son())
    play.play(GrandSon())
}

father.bar
son.bar
grandSon.bar

重寫(xiě)

面向接口編程就是多態(tài)的一種技術(shù)實(shí)現(xiàn)方式,多態(tài)的一個(gè)很顯著的特定就是運(yùn)行期綁定,也叫動(dòng)態(tài)綁定。所謂的動(dòng)態(tài)綁定就是指在程序中定義的變量,其指向的真實(shí)類(lèi)似和通過(guò)該變量發(fā)出的方法調(diào)用,在靜態(tài)編譯期并不能確定,而是要等到運(yùn)行期才能確定。
總體而言,在java和kotlin中,通常使用的最多的動(dòng)態(tài)綁定機(jī)制是通過(guò)如下三種編程技巧來(lái)實(shí)現(xiàn)的:

  • 純粹的類(lèi)繼承,如上所示
  • 子類(lèi)繼承虛類(lèi),子類(lèi)實(shí)現(xiàn)虛類(lèi)中的抽象方法
  • 面向接口編程

這三種編程技巧都能在運(yùn)行期進(jìn)行動(dòng)態(tài)綁定,在編譯期,開(kāi)發(fā)者也不知道類(lèi)變量最終會(huì)指向哪一個(gè)方向。

重載

重載發(fā)生在同一個(gè)類(lèi)中,與父類(lèi),子類(lèi)和繼承毫無(wú)關(guān)系,重載是指在同一個(gè)類(lèi)中定義了多個(gè)擁有相同名字的方法,但是這些同名方法的參數(shù)列表不同。這些函數(shù)之間其實(shí)各不相同,只是可能功能類(lèi)似,所以才采用相同的命名,增加刻度性,僅此而已。

class MyPrint{
    fun print(str:String){
        println("strValue = $str")
    }
    fun print(intValue:Int){
        println("intValue = $intValue")
    }
    fun print(longValue:Long){
        println("longValue = $longValue")
    }
    fun print(charValue:Char){
        println("charValue = $charValue")
    }
}
fun main() {
   val p = MyPrint()
    p.print("我來(lái)自大中國(guó)")
    p.print(22)
    p.print(12L)
    p.print('A')
}

strValue = 我來(lái)自大中國(guó)
intValue = 22
longValue = 12
charValue = A

擴(kuò)展

擴(kuò)展方法使你能夠向現(xiàn)有類(lèi) “添加”方法,而無(wú)需穿件新的派生類(lèi),重新編譯或以其他方式修改原始類(lèi)。
在kotlin中為核心類(lèi) 編寫(xiě)擴(kuò)展方法,下面例子為String 添加一個(gè)擴(kuò)展方法:

fun String.toMyStr():String{
    return "$this ===> add My String"
}
fun String.toFloat(digits:Int):Float{
    return try {
        this.substring(0,digits).toFloat()
    }catch (e:Exception){
        0f
    }
}

fun main() {
    val str = "12.2563"
    println(str.toMyStr())
    println(str.toFloat(5))
}

12.2563 ===> add My String
12.25

也可以給自定義類(lèi)添加擴(kuò)展函數(shù):

class MyClass(val name: String) {
    fun foo() {
        println("$name print foo")
    }
}

//無(wú)返回的擴(kuò)展函數(shù)
fun MyClass.bar() {
    //擴(kuò)展函數(shù) 可以獲取到 所在類(lèi)的字段屬性
    println("$name bar fun run ... ")
}

fun main() {
    val myCla = MyClass("王麻子")
    myCla.foo()
    myCla.bar()
}

王麻子 print foo
王麻子 bar fun run ...

擴(kuò)展與重載

看起來(lái)似乎函數(shù)擴(kuò)展無(wú)所不能,但其實(shí)也有一個(gè)內(nèi)在的限制——所擴(kuò)展的函數(shù)不能與類(lèi)內(nèi)部已有的函數(shù)簽名相同,或者套用多態(tài)里面的術(shù)語(yǔ)來(lái)說(shuō),擴(kuò)展函數(shù)不能是對(duì)類(lèi)已有函數(shù)的 “重寫(xiě)”,雖然在語(yǔ)法上允許這么做,但是在運(yùn)行期它會(huì)失效(保證安全,核心類(lèi)庫(kù)的方法不能被篡改),但是函數(shù)擴(kuò)展可以對(duì)類(lèi)已有函數(shù)進(jìn)行重載。

class MyClass(val name: String) {
    fun foo() {
        println("$name print foo")
    }
}

//無(wú)返回的擴(kuò)展函數(shù)
fun MyClass.foo() {
    //擴(kuò)展函數(shù) 可以獲取到 所在類(lèi)的字段屬性
    println("$name extends foo fun run ... ")
}

fun MyClass.foo(int: Int){
    println("extends foo print int = $int")
}

fun main() {
    val myCla = MyClass("王麻子")
    myCla.foo()
    myCla.foo(22)
}

王麻子 print foo
extends foo print int = 22

函數(shù)擴(kuò)展的多態(tài)性

在對(duì)類(lèi)方法進(jìn)行 “重載”式擴(kuò)展時(shí),如果重載函數(shù)與類(lèi)函數(shù)的入?yún)㈩?lèi)型不同,但是參數(shù)類(lèi)型之間有繼承關(guān)系,結(jié)果會(huì)怎樣呢?(不會(huì)執(zhí)行擴(kuò)展函數(shù))

open class Animal(val name: String) {

}
class Dog(name: String):Animal(name){

}

class MyClass{
    fun foo(animal: Animal){
        println("animal : ${animal.name}")
    }
}

fun MyClass.foo(dog: Dog){
    println("dog : ${dog.name}")
}

fun main() {
    val animal = Animal("小飛鼠")
    val dog = Animal("雪地三傻 之 拆家二哈")
    val cls = MyClass()
    cls.foo(animal)
    cls.foo(dog)
}

animal : 小飛鼠
animal : 雪地三傻 之 拆家二哈

kotlin這么設(shè)計(jì)的原因還是因?yàn)榘踩绻麛U(kuò)展函數(shù)可以代替核心類(lèi)庫(kù)的自身方法,那么核心類(lèi)庫(kù)將變的不安全。

  • 函數(shù)擴(kuò)展的多態(tài)性研究
open class Animal(val name: String) {

}

class Dog(name: String) : Animal(name) {

}

/**為 父類(lèi) 和 子類(lèi) 同時(shí) 擴(kuò)展同一個(gè)函數(shù)*/
fun Animal.eat() {
    println("animal $name eat food")
}

fun Dog.eat() {
    println("dog $name eat food")
}

class MyClass {
    fun foo(animal: Animal) {
        animal.eat()
    }
}

fun main() {
    val animal = Animal("小飛鼠")
    val dog = Dog("大金毛")
    val cls = MyClass()
    cls.foo(animal)
    cls.foo(dog)
}

animal 小飛鼠 eat food
animal 大金毛 eat food

為什么會(huì)出現(xiàn)這樣的情況呢?是因?yàn)閗otlin在對(duì)擴(kuò)展函數(shù)進(jìn)行綁定時(shí),使用的是“靜態(tài)綁定”機(jī)制,即在編譯期綁定,而非運(yùn)行時(shí),由于是靜態(tài)綁定,編譯期根本不知道實(shí)際運(yùn)行期的基類(lèi)變量指針究竟會(huì)指向哪一個(gè)子類(lèi),因此便索性不做任何猜想,編譯器調(diào)用的是哪一個(gè)類(lèi)的擴(kuò)展函數(shù),運(yùn)行期便也調(diào)用這個(gè)類(lèi)的擴(kuò)展函數(shù),即使這個(gè)類(lèi)實(shí)際不指向其所對(duì)應(yīng)的實(shí)例對(duì)象。由此可以進(jìn)一步得出,函數(shù)擴(kuò)展與多態(tài)之間存在本質(zhì)的區(qū)別:

  • 函數(shù)擴(kuò)展一定是靜態(tài)綁定
  • 多態(tài)的重寫(xiě)是動(dòng)態(tài)綁定

將上面的例子改成多態(tài)就可以符合我們的預(yù)期了:

open class Animal(val name: String) {
    open fun eat(){
        println("animal $name eat food")
    }
}

class Dog(name: String) : Animal(name) {
    override fun eat() {
        println("dog $name eat food")
    }
}
class MyClass {
    fun foo(animal: Animal) {
        animal.eat()
    }
}

fun main() {
    val animal = Animal("小飛鼠")
    val dog = Dog("大金毛")
    val cls = MyClass()
    cls.foo(animal)
    cls.foo(dog)
}

animal 小飛鼠 eat food
dog 大金毛 eat food

函數(shù)擴(kuò)展原理

函數(shù)擴(kuò)展并沒(méi)有向類(lèi)中間插入一個(gè)方法,也沒(méi)有改變類(lèi)的源碼,函數(shù)擴(kuò)展其實(shí)是編譯器所使用的一種 “障眼法”——當(dāng)程序被編譯后,其實(shí)這種擴(kuò)展函數(shù)就變成了一個(gè)我們平常寫(xiě)的工具方法。換言之,擴(kuò)展函數(shù)的這種聲明方式:
fun String.str2Int():Int
在編譯后會(huì)變成下面這種傳統(tǒng)的聲明方式:
fun str2Int(str:Int):Int

屬性擴(kuò)展

在擴(kuò)展類(lèi)屬性的時(shí)候,需要遵循如下3個(gè)規(guī)定:

  • 擴(kuò)展類(lèi)屬性時(shí),只能使用val關(guān)鍵字,而不能使用var關(guān)鍵字
  • 擴(kuò)展類(lèi)屬性時(shí),必須明確聲明屬性的類(lèi)型
  • 擴(kuò)展類(lèi)屬性時(shí),在get()訪(fǎng)問(wèn)器中不能使用field關(guān)鍵字段
class Animal {}
//擴(kuò)展屬性,不能使用默認(rèn)賦值的方式
val Animal.name: String
    get() = "大熊貓"

fun main() {
    val animal = Animal()
    println(animal.name)
}

操作符重載

相比于java,kotlin還提供了一種強(qiáng)大的能力——操作符重載,通過(guò)操作符重載,可以自定義兩個(gè)變量執(zhí)行相加,相乘,相除等運(yùn)算行為。
在kotlin中重載操作符很簡(jiǎn)單,只需要在函數(shù)前面添加operator關(guān)鍵字,就表示該函數(shù)是對(duì)操作符的重載。

open class Arith(var value:Int){
    /**
     * 重載乘法運(yùn)算
     */
    operator fun times(arith: Arith):Arith{
        this.value = this.value * arith.value
        return this
    }
    /**
     * 重載加法運(yùn)算
     */
    operator fun plus(int: Int):Arith{
        this.value = this.value + int
        return this
    }
    override fun toString(): String {
        return value.toString()
    }

}
fun main() {
    //本來(lái)數(shù)學(xué)的四則運(yùn)算,只能用于類(lèi)型都是數(shù)字的型的操作數(shù),而通過(guò)操作符重載,我們就可以突破這一約束
    val result = Arith(11) * Arith(9)
    println(result)
    println(Arith(6).times(Arith(12)))
    println(Arith(7) + 12)
}

99
72
19

通過(guò)擴(kuò)展函數(shù)重載操作符

在上面的例子中,對(duì)Arith 類(lèi)型重載了 plus 加法操作符,因此可以執(zhí)行 arith + 2 操作,但是如果寫(xiě)成 2+arith就會(huì)報(bào)錯(cuò)。 解決辦法就是給Int類(lèi)添加擴(kuò)展函數(shù):

operator fun Int.plus(arith: Arith):Int{
    return this + arith.value
}
fun main() {
    val result = 2 + Arith(12);
}

操作符重載原理

總體而言,操作符重載使用了下面兩種手段:

  • 如果在類(lèi)的內(nèi)部定義了操作符重載函數(shù),則kotlin會(huì)將使用該類(lèi)進(jìn)行相應(yīng)運(yùn)算的地方都替換成對(duì)重載函數(shù)的調(diào)用
  • 如果通過(guò)擴(kuò)展的方式在外部為類(lèi)重載了操作符,則kotlin會(huì)將其變成普通的方法(對(duì)應(yīng)java中的不可變靜態(tài)方法),并將使用該類(lèi)進(jìn)行相應(yīng)運(yùn)算的地方都替換成對(duì)該頂級(jí)方法的調(diào)用

如上面的Arith 示例,在其類(lèi)內(nèi)部重載了 times(),則在main 函數(shù)中,通過(guò)如下方式使用:
println(Arith(6) * Arith(12))
經(jīng)過(guò)編譯了,kotlin會(huì)將這里的乘法關(guān)鍵字進(jìn)行替換,替換成如下形式:
println(Arith(6).times(Arith(12)))

操作符重載限制

當(dāng)重載操作符運(yùn)算函數(shù)時(shí),不僅僅要在函數(shù)簽名的最前面天機(jī) “operator” 關(guān)鍵字,還需要確定正確的函數(shù)名——運(yùn)算符重載的函數(shù)名稱(chēng)是受限的,并不是想怎么命名就 怎么命名的,在kotlin中,對(duì)每一種數(shù)學(xué)運(yùn)算都規(guī)定了唯一的與之對(duì)應(yīng)的重載函數(shù)名。必須乘法就只能是 times 并有一個(gè)入?yún)?和 返回值。


一元操作符

二元操作符

數(shù)組操作符

等于操作符

函數(shù)調(diào)用

中綴符

除了可以直接重載運(yùn)算符外,kotlin 還提供了一種逆天的功能——中綴符。所謂中綴符,就是連個(gè)操作數(shù)中間的運(yùn)算符,通過(guò)中綴符,可以將如下表達(dá)式:
a + b
改成下面這種形式:
a plus b
定義一個(gè)中綴符函數(shù),必須包含關(guān)鍵字 infix ,除此之外,其他的看起來(lái)就像是一個(gè)擴(kuò)展函數(shù),下面的例子,為Int 類(lèi)型的加法運(yùn)算定義中綴符函數(shù):

//定義一個(gè)加法中綴符
infix fun Int.add(x:Int):Int{
    return this + x
}
//定義一個(gè)除法中綴符
infix fun Int.divX(y:Int):Int{
    return this / y
}
fun main() {
    val result = 2 add  22
    println(result)
    println(66 divX 22)
}

指針與傳遞

kotlin作為一種面向?qū)ο蟮木幊陶Z(yǔ)言,與java一樣,將“指針”的語(yǔ)法徹底隱藏起來(lái),不暴露給開(kāi)發(fā)者。但是指針并沒(méi)有真正的消失。而是披了個(gè)馬甲,被使了障眼法。kotlin和java都通過(guò)對(duì)象引用符號(hào)(也即類(lèi)型變量)持有對(duì)一塊連續(xù)內(nèi)存地址的引用,這種引用其實(shí)就是指針,開(kāi)發(fā)者可以通過(guò)相關(guān)的文法規(guī)則,基于這塊內(nèi)存地址訪(fǎng)問(wèn)其中處于特定偏移位置的數(shù)據(jù)和方法。而在JVM虛擬機(jī)層面,這種引用被轉(zhuǎn)換為真正的指針,物理CPU可以識(shí)別并基于此進(jìn)行內(nèi)存尋址。
在java和kotlin中,不僅可以安全的使用指針,而且還玩出了不少花樣,例如:在類(lèi)內(nèi)部,使用 this關(guān)鍵字就可以訪(fǎng)問(wèn)類(lèi)本身。再如,一個(gè)接口指針可以指向其實(shí)現(xiàn)的子類(lèi)對(duì)象實(shí)例。在調(diào)用函數(shù)時(shí),對(duì)于類(lèi)型對(duì)象也通過(guò)指針進(jìn)行傳遞。尤其在傳遞函數(shù)參數(shù)的過(guò)程中,需要區(qū)分變量的類(lèi)型——值類(lèi)型還是引用類(lèi)型,這對(duì)函數(shù)傳參特別重要——必須知道參數(shù)被傳遞進(jìn)參數(shù)之后,其值會(huì)不會(huì)被修改。

  • java中的函數(shù)參數(shù)傳遞,只是復(fù)制了一個(gè)值進(jìn)去,沒(méi)有引用,所有函數(shù)內(nèi)參數(shù)如何改變也不影響外面的參數(shù)。
  private static void swap(Integer x, Integer y){
        Integer temp = x;
        x = y;
        y = temp;
        System.out.println("swap --->  x = " + x + ", y = " + y);
    }

    public static void main(String[] args) {
        Integer x = 22;
        Integer y = 33;
        swap(x,y);
        System.out.printf(" x = %s , y = %s",x,y);
    }

swap ---> x = 33, y = 22
x = 22 , y = 33

  • 按值/引用傳遞的終結(jié)者
    由于java的 “按值傳遞”和“按引用傳遞”的概念曾經(jīng)坑過(guò)太多的人,因此kotlin干脆從語(yǔ)法層面杜絕了這種情況的發(fā)生,看如下示例:
fun swap(x:Int,y:Int){
    var temp = x;
    x = y
    y = temp
}

這個(gè)函數(shù)在kotlin中會(huì)報(bào)錯(cuò),它會(huì)提示你不能對(duì)入?yún)⑦M(jìn)行修改,這是因?yàn)樵趉otlin中,函數(shù)入?yún)⒌念?lèi)型一律變成了val,也即常量類(lèi)型,而常量類(lèi)型在kotlin中是只可讀的,而不能被修改的。kotlin通過(guò)這種機(jī)制,避免開(kāi)發(fā)者在開(kāi)發(fā)的時(shí)候,被所謂的 “按值傳遞”和“按引用傳遞”所害。如果需要做到按引用傳遞的效果,就需要包裝,包裝一個(gè)類(lèi),傳遞引用進(jìn)去,java和kotlin一樣。

class Pair(var x:Int,var y:Int){
    override fun toString(): String {
        return "x = $x , y = $y"
    }
}
fun swap(pair: Pair){
    val temp = pair.x
    pair.x = pair.y
    pair.y = temp
}
fun main() {
   val pair = Pair(22,66)
    swap(pair)
    println(pair)
}

x = 66 , y = 22

  • this指針
    在java和kotlin中,雖然沒(méi)有了指針的概念,但是變量名稱(chēng)其實(shí)就是一個(gè)指針(非基本類(lèi)型)。在JVM虛擬機(jī)內(nèi)部,變量名會(huì)被處理成指向某個(gè)地址的值。
    1.在構(gòu)造函數(shù)中使用this指針 (在類(lèi)中,this表示類(lèi)型實(shí)例對(duì)象本身)
class Language{
    var name:String = "" 
    //除了構(gòu)造函數(shù),其他普通函數(shù)也可以使用this 指向類(lèi)型實(shí)例對(duì)象本身
    constructor(name: String){
        this.name = name
    }
}

2.在內(nèi)部類(lèi)中使用this指針

open class Outer{
    override fun toString(): String {
        return "outer class... "
    }
    inner class Inner{
        override fun toString(): String {
            return "inner class"
        }

        fun foo(){
            println(this)//訪(fǎng)問(wèn)內(nèi)部類(lèi)
            println(this@Outer)//訪(fǎng)問(wèn)外部類(lèi)
        }
    }
}

fun main() {
   val inn = Outer().Inner();
    inn.foo()
}

inner class
outer class...

3.在擴(kuò)展函數(shù)中使用this指針
當(dāng)一個(gè)函數(shù)處于內(nèi)部類(lèi)中,同時(shí)該方法本身又是一個(gè)擴(kuò)展函數(shù)時(shí),情況就變得復(fù)雜了——擴(kuò)展函數(shù)本身是可以使用this關(guān)鍵字的,而內(nèi)部類(lèi)的函數(shù)也是可以使用this關(guān)鍵字的,那么在這種情況下,應(yīng)該如何正確的使用this指針呢?kotlin給出的解決方案依然是通過(guò)標(biāo)簽:

  • 在擴(kuò)展函數(shù)內(nèi)部直接使用this,則this指代被擴(kuò)展的類(lèi)型實(shí)例對(duì)象
  • 如果擴(kuò)展函數(shù)位于內(nèi)部類(lèi),則通過(guò)this加標(biāo)簽的方式分別指代內(nèi)部類(lèi)和內(nèi)部類(lèi)的宿主外部類(lèi)的實(shí)例對(duì)象
open class Outer(val value:Int){
    override fun toString(): String {
        return "outer class... "
    }
    inner class Inner(val value:Int){
        override fun toString(): String {
            return "inner class"
        }

        /**
         * 為Int 擴(kuò)展函數(shù)
         */
        fun Int.foo(){
            println(this)//訪(fǎng)問(wèn) int 實(shí)例
            println(this@Outer)//訪(fǎng)問(wèn)外部類(lèi)
            println(this@Inner)//訪(fǎng)問(wèn)內(nèi)部類(lèi)
            println(this@Outer.value)//訪(fǎng)問(wèn)外部類(lèi)屬性
            println(this@Inner.value)//訪(fǎng)問(wèn)內(nèi)部類(lèi)屬性
        }

        fun foo(){
            val a = 3
            a.foo()
        }
    }
}

fun main() {
   val inn = Outer(12).Inner(55)
    inn.foo()
}

3
outer class...
inner class
12
55

  • 函數(shù)調(diào)用機(jī)制與this
    在類(lèi)內(nèi)部的函數(shù)中可以訪(fǎng)問(wèn)類(lèi)的屬性,可以可以調(diào)用該類(lèi)的其他函數(shù),通過(guò)this調(diào)用。那么在運(yùn)行期間是如何處理this的,JVM虛擬機(jī)怎樣將其與類(lèi)實(shí)例對(duì)象進(jìn)行關(guān)聯(lián)呢?
open class MyClass(val value:Int){
    override fun toString(): String {
        return this.value.toString()
    }
    fun print(){
        println(this.toString())
    }
}

fun main() {
  val cls = MyClass(44)
    cls.print()
}

在上面這個(gè)例子中,MyClass.toString() 方法內(nèi)部通過(guò)this訪(fǎng)問(wèn)了該類(lèi)的value屬性,同時(shí)該類(lèi)的print()方法內(nèi)部通過(guò)this調(diào)用了該類(lèi)重寫(xiě)的toString()方法。
為了處理this與實(shí)際所實(shí)例化處理的MyClass對(duì)象之間的綁定關(guān)系,編譯器就偷偷的做了手腳,首先編譯器會(huì)修改你所定義的每一個(gè)類(lèi)方法,為這些方法增加一個(gè)入?yún)ⅲ撊雲(yún)㈩?lèi)型就是方法所屬的類(lèi)型,并且編譯器會(huì)將該入?yún)⒉迦氲椒椒ǖ牡谝粋€(gè)參數(shù)位置,該方法原有的參數(shù)都會(huì)往后順延。以這個(gè)類(lèi)為例子編譯后如下:

  override fun toString(MyClass cls): String {
        return cls.value.toString()
    }
    fun print(MyClass cls){
        println(cls.toString())
    }

接著這些方法的調(diào)用也會(huì)被修改成正確的傳參方式。例如本例中在main()函數(shù)中調(diào)用了類(lèi)方法,則編譯器會(huì)對(duì)其進(jìn)行修改,修改后的main()函數(shù)變成:

fun main(){
  val cls = MyClass(44)
  print(cls)
}

kotlin 正是通過(guò)這種“騰挪”第一個(gè)入?yún)⒌臋C(jī)制,完成類(lèi)方法內(nèi)部this指針與類(lèi)實(shí)例對(duì)象之間的綁定的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容