JAVA老鳥學Kotlin(一):lambda表達式和高階函數操做符

前言

以一個java老鳥的角度,如何去看 kotlin。 Java源代碼應該如何用Kotlin重構。 如何正確學習kotlin而且應用到實際開發中。本文將會探究。java

本文分兩大塊,重難點和潛規則。android

重難點:Kotlin中能夠獨立出來說解的大塊知識點。提供單獨Demo。這部分大多數是Kotlin開創的新概念(相比於Java)。安全

潛規則:Kotlin是谷歌用來替換Java的,它和java百分百徹底兼容,可是實際上java轉成kotlin以後,須要咱們手動修改不少東西,甚至某些部分必須打散重構來達到最優編碼。其中,kotlin的某些特性和java不一樣,甚至徹底反轉。這部分知識點比較零碎,單獨Demo不方便提供,就以小例子的形式來寫。markdown

正文大綱

  • 重難點app

    • lambda以及操做符
    • 高階函數以及操做符
    • Kotlin泛型
    • 集合操做
    • 協程
    • 操做符重載
  • 潛規則框架

    • Kotlin文件和類不存在一對一關係less

    • 共生體ide

    • 繼承函數

    • 修飾符學習

    • 空指針問題

正文

重難點

lambda表達式

lambda表達式是JDK1.8提出的,目的是爲了改善Java中大量出現的只有一個方法的回調函數的累贅寫法。這裏不討論Java的lambda. Kotlin中lambda已經融入代碼核心,而不是像java同樣是個註解+語法糖。

基礎:

一個lambda表達式如圖:

lambda 表達式的4個部分

  • 外圍的{}大括號
  • 參數列表:x:Int,y:Int
  • 鏈接符 ->
  • 方法體 x+y

舉個栗子

這是一個kotlin文件:

/** * 好比說這樣,我要給這個doFirst方法傳一個執行過程,類型就是一個輸入2個Int,輸出一個Int的拉姆達表達式 */
fun calculate(p1: Int, p2: Int, event1: (Int, Int) -> Int, event2: (Int, Int) -> Int) {
    println("執行doFirst")
    println("執行event1 ${event1(p1, p2)}")
    println("執行event2 ${event2(p1, p2)}")
}

//測試lambda表達式
fun main() {
    val sum = { x: Int, y: Int ->
        print("求和 ")
        x + y
    }
    val diff = { x: Int, y: Int ->
        print("求差 ")
        x - y
    }
    //kotlin裏面,能夠把拉姆達表示看成一個普通變量同樣,去傳遞實參
    calculate(p1 = 1, p2 = 2, event1 = sum, event2 = diff)
}
複製代碼

定義了一個calculate函數, p1,p2 是Int,而event1和event2 則是 lambda表達式. 高級語言特性,一等函數公民:函數自己能夠被看成普通參數同樣去傳遞,而且調用。那麼, kotlin的lambda內核是怎麼樣的?

上圖可以看出,

  1. calculate方法的後面2個參數,被編譯成了 Function2 類型。
  2. 執行event1,event2,則是調用其invoke方法
  3. main函數中,出現了null.INSTANCE, 這裏比較詭異,INSTANCE應該是用來獲取實例的,可是爲何是null.INSTANCE

而看了Function2的源碼,簡直亮瞎了個人鈦合金狗眼:

Function.kt文件中:

Function接口,Function2接口.....Function22接口。好了,不要質疑谷歌大佬的設計思路,反正大佬寫的就是對的...這裏用22個接口(至於爲何是22個,猜測是谷歌以爲不會有哪一個腦子秀逗的開發者真的弄22個以上的參數擠在一塊兒吧),表示kotlin開發中可能出現的lambda表達式參數列表。

再舉個栗子

給一個Button 設置點擊事件,kotlin的寫法是:

val btn = Button(this)
val lis = View.OnClickListener { println("111") }
btn.setOnClickListener(lis)
複製代碼

或者:

val btn = Button(this)
btn.setOnClickListener { println("111") }
複製代碼

setOnClickListener 方法的參數是 OnClickListener 接口:

public interface OnClickListener {
    void onClick(View v);
}
複製代碼

相似這種符合lambda表達式特徵的接口,均可以使用上述兩種寫法來大大簡化代碼量。

最後一個栗子

不得不提一句,lambda表達式有一個變形,那就是:當lambda表達式做爲方法的最後一個參數時,能夠lambda表達式放到小括號外面。而若是隻有一個參數就是lambda表達式,那麼括號能夠省略

這個很是重要,不瞭解這個,不少地方都會感受很蛋疼。

fun testLambda(s: String, block: () -> String) {
    println(s)
    block()
}

fun testLambda2(block: () -> String) {
    block()
}

fun main() {
    testLambda("第一個參數") {
        println("block函數體")
        "返回值"
    }
    testLambda2 {
        println("block函數體")
        "返回值"
    }
}
複製代碼

總結

Kotlin中lambda表達式能夠看成普通參數同樣去傳遞,去賦值,去使用。


高階函數以及操做符

上文提到,kotlin中lambda表達式已經融入了語言核心,而具體的體現就是高階函數,把lambda表達式看成參數去使用. 這種將lambda表達式做爲參數或者返回值的函數,就叫高階函數。

官方高階函數

Kotlin谷歌已經給咱們封裝了一些高階函數。

  • run
  • with
  • apply
  • also
  • let
  • takeif 和 takeunless
  • repeat
  • lazy

run函數詳解

代碼以下(這裏爲了展現代碼全貌,忽視androidStudio的代碼優化提示):

class A {
    val a = 1
    val b = "b"
}
fun testRun() {
    //run方法有兩種用法,一個是不依賴對象,也就是做爲全局函數
    run<String> {//我能夠規定返回值的類型
        println("我是全局函數")
        "返回值"
    }
    val a = A()
    //另外一種則是 依賴對象
    a.run<A,String> {//這裏一樣能夠規定返回值的類型
        println(this.a)
        println(this.b)
        "返回值"
    }
}
fun main() {
    testRun()
}
複製代碼

如上所示:

run函數分爲兩類

  • 不依賴對象的全局函數。

  • 依賴對象的"相似"擴展函數。

    二者均可以規定返回值類型(精通泛型的話,這裏應該不難理解,泛型下一節詳解)。

閱讀源碼:

@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製代碼

run函數被重載,參數有所不一樣

  • 前者 參數類型爲 ()->R ,返回值爲 R ,函數體內執行block(),而且返回執行結果
  • 後者 參數類型爲 T.()->R ,返回值爲R , T.() 明顯是依賴 T類的(貌似T的擴展函數),返回值依然是R,執行完以後返回結果。
  • 而且,能夠發現 二者都是內聯函數 inline (執行代碼時不進行方法的壓棧出棧,而是相似於直接在目標處執行代碼段)

因此,前者不須要依賴對象,後者必須依賴對象(由於它是T類的"擴展函數")

使用場景

根據run方法的特性,不管是否是依賴對象,它都是封裝了一段代碼,並且仍是inline的。因此:

  • 若是你不想把一段代碼獨立成方法,又不想讓它們看上去這麼凌亂,最重要的是告訴後來的開發者 這段代碼是一個總體,不要隨便給我在裏面插代碼,或者拆分。那就用run方法,把他們整合到一個做用域中。

    run {
        println("這是一段代碼邏輯很相近的代碼段")
        println("可是我不想把他獨立成一個方法")
        println("又擔憂別人會隨便改")
        println("因此用run方法把他們放在同一個做用域中")
        println("小組中其餘人看到這段,就知道不要把無關代碼插進來")
    }
    複製代碼
  • 更神奇的是,這個run函數是有返回值的,參數返回值能夠利用起來:

    fun testRun2(param1: String, param2: String, param3: String) {
        //我想讓這幾個參數都不爲空,若是檢查是空,就不執行方法主體
        val checkRes: Boolean = run<Boolean> {
            when {
                param1.isNullOrEmpty() -> {
                    false
                }
                param2.isNullOrEmpty() -> {
                    false
                }
                param3.isNullOrEmpty() -> {
                    false
                }
                else -> true
            }
        }
    
        if (checkRes){
            println("參數檢查完畢,如今能夠繼續接下來的操做了")
        }else{
            println("參數檢查不經過,不執行主體代碼")
        }
    }
    fun main() {
      testRun2("1", "2", "")
    }
    複製代碼

main運行結果:

參數檢查完畢,如今能夠繼續接下來的操做了

小結論

run方法在整合小段代碼的功效上,仍是很實用的

其餘高階函數

上面列舉出來的這些系統高階函數原理上都差很少,只是使用場景有區別,所以除了run以外,其餘的再也不詳解,而只說明使用場景。

apply

和run只有一個區別,run是返回block()執行以後的返回值,而,apply 則是返回this,所以 apply必須依賴對象。而因爲返回了this,所以能夠連續apply調用。

fun testApply() {
    val a = A()
    a.apply {
        println("若是一個對象要對它進行多個階段的處理")
    }.apply {
        println("那麼多個階段都擠在一塊兒則容易混淆,")
    }.apply {
        println("此時能夠用apply將每個階段分開擺放")
    }.apply {
        println("讓程序代碼更加優雅")
    }
}

fun main() {
    testApply()
}
複製代碼
with

下面的代碼應該都很眼熟,Glide圖片加載框架的用法,with(context)而後鏈式調用

Glide.with(this).load(image).asGif().into(mImageView);
複製代碼

Kotlin中的with貌似把這一寫法發揚光大了(只是類比,不用深究),場景以下:

class A {
    val a = 1
    val b = "b"

    fun showA() {
        println("$a")
    }

    fun showB() {
        println("$a $b")
    }
}

fun testWith() {
    val a = A()
    with(a) {
        println("做用域中能夠直接引用建立出的a對象")
        this.a
        this.b
        this.showA()
        this
    }.showB()
}

fun main() {
    testWith()
}
複製代碼

細節

  1. with(a){} 大括號內的做用域裏面,能夠直接使用 當前a對象的引用,能夠this.xxx 也能夠 a.xxx
  2. with(a){} 大括號做用域內的最後一行是 返回值,若是我返回this,那麼with結束以後,我能夠繼續 調用a的方法
also

also和with同樣,必須依賴對象,返回值爲this。所以它也支持鏈式調用,它和apply的區別是:

apply的block,沒有參數,可是 also 則將this做爲了參數。這麼作形成的差別是:

做用域 { } 內調用當前對象的方式不一樣。

class A {
    val a = 1
    val b = "b"

    fun showA() {
        println("$a")
    }

    fun showB() {
        println("$a $b")
    }
}
fun testApply() {
    A().apply {
        this.showA()
        println("=======")
    }.showB()
}

fun testAlso() {
    A().also {
        it.showA()
        println("=======")
    }.showB()
}
複製代碼

apply 必須用this.xxx, also則用 it.xxx.

let

類比到run:

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}
複製代碼

只有一個區別:run的block執行不須要參數,而let 的block執行時須要傳入this。

形成差別爲:

A().run {
    println("最後一行爲返回值")
    this
}.showA()

A().let {
    println("最後一行爲返回值")
    it
}.showA()
複製代碼

run{} 做用域中 只能經過this.xxx操做當前對象,let 則用 it.xxx

takeif 和 takeunless

這兩個做用相反,而且他們必須依賴對象。看源碼:

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    return if (!predicate(this)) this else null
}
複製代碼

predicate 是 (T)->Boolean 類型的lambda表達式,表示斷言判斷,若是判斷爲true,則返回自身,不然返回空

class A {
    val a = 0
    val b = "b"

    fun showA() {
        println("$a")
    }

    fun showB() {
        println("$a $b")
    }
}

fun testTakeIfAndTakeUnless() {
    println("test takeIf")
    A().takeIf { it.a > 0 }?.showB()
    println("==========")
    println("test takeUnless")
    A().takeUnless { it.a > 0 }?.showB()
}

fun main() {
    testTakeIfAndTakeUnless()
}
複製代碼

執行結果:

test takeIf
==========
test takeUnless
0 b
複製代碼

takeIf / takeUnless適用於將條件判斷的代碼包在一個做用域{}中,而後 用 ?.xxxx的安全調用符 來 執行對象操做。

repeat

repeat是 屢次循環的傻瓜版寫法。

fun testRepeat() {
    repeat(10) {
        print("$it ")
    }
}

fun main() {
    testRepeat()
}
複製代碼

執行結果:

0 1 2 3 4 5 6 7 8 9 
複製代碼
lazy

lazy的做用是: 延遲初始化val定義的常量。

class B {
    val i: Int by lazy {
        println("執行i初始化")
        20
    }

    init {
        println("構造函數執行")
    }
}
複製代碼

若是隻是初始化B對象,卻沒有使用到變量i, 那麼延遲初始化不會執行。

fun main() {
    B()
}
複製代碼

執行結果:

構造函數執行
複製代碼

而若是使用到了變量i,纔會去在調用以前初始化:

fun main() {
    println("i 變量的值是:" + B().i)
}
複製代碼

執行結果:

構造函數執行
執行i初始化
i 變量的值是:20
複製代碼

總結

Kotlin官方提供的高階函數,run,apply,also,let,with等,旨在使用**{}做用域**,將聯繫緊密的代碼封在一個做用域內,讓一個方法內部顯得 有條不紊,閱讀觀感更好,可維護性更高,代碼更優雅。上述,除了lazy以外,全部的 函數都在Standart.kt文件內部。

相關文章
相關標籤/搜索