讀書筆記《Kotlin核心編程》

dive-into-kotlin.jpg

1. 重點(diǎn)理解val的使用規(guī)則

引用1

如果說(shuō)var代表了varible(變量),那么val可看成value(值)的縮寫。但也有人覺得這樣并不直觀或準(zhǔn)確,而是把val解釋成varible+final,即通過(guò)val聲明的變量具有Java中的final關(guān)鍵字的效果,也就是引用不可變。

val聲明的變量是只讀變量,它的引用不可更改,但并不代表其引用對(duì)象也不可變。事實(shí)上,我們依然可以修改引用對(duì)象的可變成員。

引用2

優(yōu)先使用val來(lái)避免副作用

在很多Kotlin的學(xué)習(xí)資料中,都會(huì)傳遞一個(gè)原則:優(yōu)先使用val來(lái)聲明變量。這相當(dāng)正確,但更好的理解可以是:盡可能采用val、不可變對(duì)象及純函數(shù)(其實(shí)就是沒(méi)有副作用的函數(shù),具備引用透明性)來(lái)設(shè)計(jì)程序。

引用3

然而,在Kotlin編程中,我們推薦優(yōu)先使用val來(lái)聲明一個(gè)本身不可變的變量,這在大部分情況下更具有優(yōu)勢(shì):

? 這是一種防御性的編碼思維模式,更加安全和可靠,因?yàn)樽兞康闹涤肋h(yuǎn)不會(huì)在其他地方被修改(一些框架采用反射技術(shù)的情況除外);

? 不可變的變量意味著更加容易推理,越是復(fù)雜的業(yè)務(wù)邏輯,它的優(yōu)勢(shì)就越大。

點(diǎn)評(píng)

上面說(shuō)的其實(shí)非常明確了,val聲明的變量具有Java中的final關(guān)鍵字的效果,也就是引用不可變,但其引用的內(nèi)容是可變的。其實(shí)這里扯出了兩個(gè)概念對(duì)我來(lái)說(shuō)更重要,一個(gè)是變量或函數(shù)的副作用,一個(gè)是防御性編程思維。

在后續(xù)編程中,會(huì)注意到變量副作用這塊,盡量避免。而防御性思維其實(shí)一直都有,對(duì)于外部輸入的參數(shù),總是站在不可靠的角度上對(duì)待,從而寫出可靠的代碼。

2. 關(guān)于函數(shù)和Lambda

引用1

Kotlin天然支持了部分函數(shù)式特性。函數(shù)式語(yǔ)言一個(gè)典型的特征就在于函數(shù)是頭等公民——我們不僅可以像類一樣在頂層直接定義一個(gè)函數(shù),也可以在一個(gè)函數(shù)內(nèi)部定義一個(gè)局部函數(shù)。

引用2

所謂的高階函數(shù),你可以把它理解成“以其他函數(shù)作為參數(shù)或返回值的函數(shù)”。高階函數(shù)是一種更加高級(jí)的抽象機(jī)制,它極大地增強(qiáng)了語(yǔ)言的表達(dá)能力。

引用3

Kotlin存在一種特殊的語(yǔ)法,通過(guò)兩個(gè)冒號(hào)來(lái)實(shí)現(xiàn)對(duì)于某個(gè)類的方法進(jìn)行引用。

為什么使用雙冒號(hào)的語(yǔ)法?

如果你了解C#,會(huì)知道它也有類似的方法引用特性,只是語(yǔ)法上不同,是通過(guò)點(diǎn)號(hào)來(lái)實(shí)現(xiàn)的。然而,C#的這種方式存在二義性,容易讓人混淆方法引用表達(dá)式與成員表達(dá)式,所以Kotlin采用::(沿襲了Java 8的習(xí)慣),能夠讓我們更加清晰地認(rèn)識(shí)這種語(yǔ)法。

引用4

Lambda表達(dá)式,你可以把它理解成簡(jiǎn)化表達(dá)后的匿名函數(shù),實(shí)質(zhì)上它就是一種語(yǔ)法糖。

現(xiàn)在來(lái)總結(jié)下Lambda的語(yǔ)法:

? 一個(gè)Lambda表達(dá)式必須通過(guò){}來(lái)包裹;

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

? 如果Lambda聲明了參數(shù)部分的類型,且返回值類型支持類型推導(dǎo),那么Lambda變量就可以省略函數(shù)類型聲明;

val sum = { x: Int, y: Int -> x + y }

? 如果Lambda變量聲明了函數(shù)類型,那么Lambda的參數(shù)部分的類型就可以省略。

val sum: (Int, Int) -> Int = { x, y -> x + y }

引用5

區(qū)分函數(shù)、Lambda

? fun在沒(méi)有等號(hào)、只有花括號(hào)的情況下,是我們最常見的代碼塊函數(shù)體,如果返回非Unit值,必須帶return。

fun foo(x: Int) { print(x) }
fun foo(x: Int, y: Int): Int { return x * y }

? fun帶有等號(hào),是單表達(dá)式函數(shù)體。該情況下可以省略return。

fun foo(x: Int, y: Int) = x + y

? 不管是用val還是fun,如果是等號(hào)加花括號(hào)的語(yǔ)法,那么構(gòu)建的就是一個(gè)Lambda表達(dá)式,Lambda的參數(shù)在花括號(hào)內(nèi)部聲明。所以,如果左側(cè)是fun,那么就是Lambda表達(dá)式函數(shù)體,也必須通過(guò)()或invoke來(lái)調(diào)用Lambda。

val foo = { x: Int, y: Int -> x + y } // foo.invoke(1, 2)或foo(1, 2)
fun foo(x: Int) = { y: Int -> x + y } // foo(1).invoke(2)或foo(1)(2)

點(diǎn)評(píng)

這部分內(nèi)容對(duì)我個(gè)人而言,是區(qū)分函數(shù)和lambda。之前沒(méi)有仔細(xì)思考擴(kuò)這個(gè)問(wèn)題,一直是憑著感覺來(lái)。這次算是理清楚了:如果是等號(hào)加花括號(hào)的語(yǔ)法,那么構(gòu)建的就是一個(gè)Lambda表達(dá)式。那么調(diào)用時(shí)就必須通過(guò)()或invoke來(lái)實(shí)現(xiàn)。

還有一點(diǎn)是對(duì)函數(shù)是頭等公民的理解,之前也聽過(guò)這句話,但是具體體現(xiàn)在哪不得而知。你見過(guò)在Java中,函數(shù)里面在定義一個(gè)函數(shù)嗎?你見過(guò)在Java類外部定義一個(gè)函數(shù)嗎?沒(méi)有吧,因?yàn)镴ava中對(duì)象是頭等公民。而Kt中你可以在函數(shù)中再定義函數(shù),再類頂層定義函數(shù),這就是區(qū)別。

3. 表達(dá)式

引用

.. 閉區(qū)間 , until 半開區(qū)間

for(i in 1 until 10){
    print(i) // 123456789
}
println()

for(i in 1..10){
    print(i) // 12345678910
}
println()

for(i in 0..0){
    print(i) // output 0
}
println()

for(i in 0 until 0){
    print(i) // nothing 
}

點(diǎn)評(píng)

關(guān)于until半開區(qū)間的特性,自己還在工作過(guò)程中犯過(guò)一個(gè)錯(cuò)誤,需求其實(shí)很簡(jiǎn)單,就是在一個(gè)范圍內(nèi)獲取一個(gè)隨機(jī)數(shù),但是當(dāng)你寫出這句時(shí) (0 until 0).random(),就有bug在等著你了。

上面實(shí)例代碼中其實(shí)做了實(shí)驗(yàn),打印 0 until 0 是沒(méi)有任何內(nèi)容輸出的,再去向無(wú)任何輸出內(nèi)容的表達(dá)式要一個(gè)隨機(jī)數(shù),編譯器不報(bào)錯(cuò)能干嘛呢?

print((0 until 0).random()) // Exception in thread "main" java.util.NoSuchElementException: Cannot get random in empty range: 0..-1
print((0..0).random()) // 0 

4. init語(yǔ)句塊

引用1

Kotlin引入了一種叫作init語(yǔ)句塊的語(yǔ)法,它屬于構(gòu)造方法的一部分,兩者在表現(xiàn)形式上卻是分離的。Bird類的構(gòu)造方法在類的外部,它只能對(duì)參數(shù)進(jìn)行賦值。如果我們需要在初始化時(shí)進(jìn)行其他的額外操作,那么我們就可以使用init語(yǔ)句塊來(lái)執(zhí)行。

當(dāng)沒(méi)有val或var的時(shí)候,構(gòu)造方法的參數(shù)可以在init語(yǔ)句塊被直接調(diào)用。其實(shí)它們還可以用于初始化類內(nèi)部的屬性成員的情況。

class Bird(weight: Double = 0.00, age: Int = 0,color: String = "blue") {
   val weight: Double = weight //在初始化屬性成員時(shí)調(diào)用weight
   val age: Int = age
   val color: String = color
}

除此之外,我們并不能在其他地方使用。

class Bird(weight: Double, age: Int, color: String) {
   fun printWeight() {
       print(weight) // Unresolved reference: weight
   }
}

事實(shí)上,我們的構(gòu)造方法還可以擁有多個(gè)init,它們會(huì)在對(duì)象被創(chuàng)建時(shí)按照類中從上到下的順序先后執(zhí)行。多個(gè)init語(yǔ)句塊有利于我們進(jìn)一步對(duì)初始化的操作進(jìn)行職能分離,這在復(fù)雜的業(yè)務(wù)開發(fā)(如Android)中顯得特別有用。

引用2

我們?cè)贐ird類中可以用val或者var來(lái)聲明構(gòu)造方法的參數(shù)。這一方面代表了參數(shù)的引用可變性,另一方面它也使得我們?cè)跇?gòu)造類的語(yǔ)法上得到了簡(jiǎn)化。

為什么這么說(shuō)呢?事實(shí)上,構(gòu)造方法的參數(shù)名前當(dāng)然可以沒(méi)有val和var,然而帶上它們之后就等價(jià)于在Bird類內(nèi)部聲明了一個(gè)同名的屬性,我們可以用this來(lái)進(jìn)行調(diào)用。比如我們前面定義的Bird類就類似于以下的實(shí)現(xiàn):

class Bird(
        weight: Double = 0.00, // 參數(shù)名前沒(méi)有val
        age: Int = 0,
        color: String = "blue") {
    val weight: Double
    val age: Int
    val color: String
    init {
        this.weight = weight // 構(gòu)造方法參數(shù)可以在init語(yǔ)句塊被調(diào)用
        this.age = age
        this.color = color
    }
}

點(diǎn)評(píng)

這一部分的內(nèi)容對(duì)我個(gè)人而言是知識(shí)盲區(qū),這次算是補(bǔ)上了。用val或var修飾的構(gòu)造方法參數(shù),實(shí)際上等價(jià)于在類內(nèi)部聲明了一個(gè)同名屬性,可以用this進(jìn)行調(diào)用。

5. 關(guān)于延遲初始化

引用1

總結(jié)by lazy語(yǔ)法的特點(diǎn)如下

? 該變量必須是引用不可變的,而不能通過(guò)var來(lái)聲明。

? 在被首次調(diào)用時(shí),才會(huì)進(jìn)行賦值操作。一旦被賦值,后續(xù)它將不能被更改。

class Bird(val weight: Double, val age: Int, val color: String) {
    val sex: String by lazy {
        if (color == "yellow") "male" else "female"
    }
}

另外系統(tǒng)會(huì)給lazy屬性默認(rèn)加上同步鎖,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一時(shí)刻只允許一個(gè)線程對(duì)lazy屬性進(jìn)行初始化,所以它是線程安全的。但若你能確認(rèn)該屬性可以并行執(zhí)行,沒(méi)有線程安全問(wèn)題,那么可以給lazy傳遞LazyThreadSafetyMode.PUBLICATION參數(shù)。你還可以給lazy傳遞LazyThreadSafetyMode. NONE參數(shù),這將不會(huì)有任何線程方面的開銷,當(dāng)然也不會(huì)有任何線程安全的保證。

val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
    //并行模式
    if (color == "yellow") "male" else "female"
}
val sex: String by lazy(LazyThreadSafetyMode.NONE) {
    //不做任何線程保證也不會(huì)有任何線程開銷
    if (color == "yellow") "male" else "female"
}

引用2

總結(jié)lateinit語(yǔ)法特點(diǎn)如下

? 主要用于var聲明的變量,然而它不能用于基本數(shù)據(jù)類型,如Int、Long等,我們需要用Integer這種包裝類作為替代。

class Bird(val weight: Double, val age: Int, val color: String) {
    lateinit var sex: String // sex可以延遲初始化
    fun printSex() {
        this.sex = if (this.color == "yellow") "male" else "female"
        println(this.sex)
    }
}
fun main(args: Array<String>) {
    val bird = Bird(1000.0, 2, "bule")
    bird.printSex()
}
// 運(yùn)行結(jié)果
female

引用3

你可能比較好奇,如何讓用var聲明的基本數(shù)據(jù)類型變量也具有延遲初始化的效果,一種可參考的解決方案是通過(guò)Delegates.notNull<T>,這是利用Kotlin中委托的語(yǔ)法來(lái)實(shí)現(xiàn)的。

var test by Delegates.notNull<Int>()
fun doSomething() {
    test = 1
    println("test value is ${test}")
    test = 2
}

點(diǎn)評(píng)

這塊主要是總結(jié)by lazy和lateinit的區(qū)別,根據(jù)兩者的區(qū)別選擇合適的延遲方案很重要。簡(jiǎn)單粗暴一點(diǎn)就是by lazy對(duì)應(yīng)val,lateint對(duì)應(yīng)var。

6. 可見性修飾符

引用

可見性修飾符對(duì)比

Kotlin中的可見性修飾符也與Java中的很類似。但也有不一樣的地方,主要有以下幾點(diǎn):

1)Kotlin與Java的默認(rèn)修飾符不同,Kotlin中是public,而Java中是default,它只允許包內(nèi)訪問(wèn)。

2)Kotlin中有一個(gè)獨(dú)特的修飾符internal。

3)Kotlin可以在一個(gè)文件內(nèi)單獨(dú)聲明方法及常量,同樣支持可見性修飾符。

4)Java中除了內(nèi)部類可以用private修飾以外,其他類都不允許private修飾,而Kotlin可以。

5)Kotlin和Java中的protected的訪問(wèn)范圍不同,Java中是包、類及子類可訪問(wèn),而Kotlin只允許類及子類。

關(guān)于internal

Kotlin中有一個(gè)獨(dú)特的修飾符internal,和default有點(diǎn)像但也有所區(qū)別。internal在Kotlin中的作用域可以被稱作“模塊內(nèi)訪問(wèn)”。那么到底什么算是模塊呢?以下幾種情況可以算作一個(gè)模塊
? 一個(gè)Eclipse項(xiàng)目

? 一個(gè)Intellij IDEA項(xiàng)目

? 一個(gè)Maven項(xiàng)目

? 一個(gè)Grandle項(xiàng)目

? 一組由一次Ant任務(wù)執(zhí)行編譯的代碼

總的來(lái)說(shuō),一個(gè)模塊可以看作一起編譯的Kotlin文件組成的集合。那么,Kotlin中為什么要誕生這么一種新的修飾符呢?Java的包內(nèi)訪問(wèn)不好嗎?

Java的包內(nèi)訪問(wèn)中確實(shí)存在一些問(wèn)題。舉個(gè)例子,假如你在Java項(xiàng)目中定義了一個(gè)類,使用了默認(rèn)修飾符,那么現(xiàn)在這個(gè)類是包私有,其他地方將無(wú)法訪問(wèn)它。然后,你把它打包成一個(gè)類庫(kù),并提供給其他項(xiàng)目使用,這時(shí)候如果有個(gè)開發(fā)者想使用這個(gè)類,除了copy源代碼以外,還有一個(gè)方式就是在程序中創(chuàng)建一個(gè)與該類相同名字的包,那么這個(gè)包下面的其他類就可以直接使用我們前面的定義的類。這樣我們便可以直接訪問(wèn)該類了。

而Kotlin默認(rèn)并沒(méi)有采用這種包內(nèi)可見的作用域,而是使用了模塊內(nèi)可見,模塊內(nèi)可見指的是該類只對(duì)一起編譯的其他Kotlin文件可見。開發(fā)工程與第三方類庫(kù)不屬于同一個(gè)模塊,這時(shí)如果還想使用該類的話只有復(fù)制源碼一種方式了。這便是Kotlin中internal修飾符的一個(gè)作用體現(xiàn)。

關(guān)于private

在Java程序中,我們很少見到用private修飾的類,因?yàn)镴ava中的類或方法沒(méi)有單獨(dú)屬于某個(gè)文件的概念。比如,我們創(chuàng)建了Rectangle.java這個(gè)文件,那么它里面的類要么是public,要么是包私有,而沒(méi)有只屬于這個(gè)文件的概念。若要用private修飾,那么這個(gè)只能是其他類的內(nèi)部類。而Kotlin中則可以用private給單獨(dú)的類修飾,它的作用域就是當(dāng)前這個(gè)Kotlin文件。

關(guān)于protected

Java中的protected修飾的內(nèi)容作用域是包內(nèi)、類及子類可訪問(wèn),而在Kotlin中,由于沒(méi)有包作用域的概念,所以protected修飾符在Kotlin中的作用域只有類及子類。

在了解了Kotlin中的可見修飾符后,我們來(lái)思考一個(gè)問(wèn)題:前面已經(jīng)講解了為什么要誕生internal這個(gè)修飾符,那么為什么Kotlin中默認(rèn)的可見性修飾符是public,而不是internal呢?

關(guān)于這一點(diǎn),Kotlin的開發(fā)人員在官方論壇進(jìn)行了說(shuō)明,這里我做一個(gè)總結(jié):Kotlin通過(guò)分析以往的大眾開發(fā)的代碼,發(fā)現(xiàn)使用public修飾的內(nèi)容比其他修飾符的內(nèi)容多得多,所以Kotlin為了保持語(yǔ)言的簡(jiǎn)潔性,考慮多數(shù)情況,最終決定將public當(dāng)作默認(rèn)修飾符。

點(diǎn)評(píng)

Kotlin與Java的可見性修飾符比較這一部分是我強(qiáng)烈推薦仔細(xì)閱讀的部分,可見性修飾符雖然簡(jiǎn)單但卻非常重要,kotlin的internal、private、protected修飾符都有自身獨(dú)特的特點(diǎn),跟你原本掌握的Java有很大的不同。

對(duì)比學(xué)習(xí)可能會(huì)讓我們對(duì)兩門語(yǔ)言的理解層次更深。

Kotlin中沒(méi)有包內(nèi)可見這種作用域,轉(zhuǎn)而代之的是模塊內(nèi)可見,這種方式對(duì)比Java中的包內(nèi)可見在某種意義上可能會(huì)更加“安全”。

另一邊Java中某個(gè)類或方法沒(méi)有單獨(dú)屬于某個(gè)文件的概念,而Kotlin中則可以用private單獨(dú)修飾某個(gè)類,它的作用域就是當(dāng)前這個(gè)kotlin文件,這種設(shè)計(jì)在我看來(lái)可能會(huì)讓你更加能精準(zhǔn)控制某個(gè)類的訪問(wèn)權(quán)限。

7. getter和setter

引用

1)用val聲明的屬性將只有g(shù)etter方法,因?yàn)樗豢尚薷?;而用var修飾的屬性將同時(shí)擁有g(shù)etter和setter方法。

2)用private修飾的屬性編譯器將會(huì)省略getter和setter方法,因?yàn)樵陬愅獠恳呀?jīng)無(wú)法訪問(wèn)它了,這兩個(gè)方法的存在也就沒(méi)有意義了。

8. 內(nèi)部類vs嵌套類

引用

眾所周知,在Java中,我們通過(guò)在內(nèi)部類的語(yǔ)法上增加一個(gè)static關(guān)鍵詞,把它變成一個(gè)嵌套類。然而,Kotlin則是相反的思路,默認(rèn)是一個(gè)嵌套類,必須加上inner關(guān)鍵字才是一個(gè)內(nèi)部類,也就是說(shuō)可以把靜態(tài)的內(nèi)部類看成嵌套類。

內(nèi)部類和嵌套類有明顯的差別,具體體現(xiàn)在:內(nèi)部類包含著對(duì)其外部類實(shí)例的引用,在內(nèi)部類中我們可以使用外部類中的屬性;而嵌套類不包含對(duì)其外部類實(shí)例的引用,所以它無(wú)法調(diào)用其外部類的屬性。

open class Horse { //馬
    fun runFast() {
        println("I can run fast")
    }
}
open class Donkey { //驢
    fun doLongTimeThing() {
        println("I can do some thing long time")
    }
}
class Mule {  //騾子
    fun runFast() {
        HorseC().runFast()
    }
    fun doLongTimeThing() {
        DonkeyC().doLongTimeThing()
    }
    private inner class HorseC : Horse()
    private inner class DonkeyC : Donkey()
}

9. 數(shù)據(jù)類的約定與使用

引用

如果你要在Kotlin聲明一個(gè)數(shù)據(jù)類,必須滿足以下幾點(diǎn)條件:

? 數(shù)據(jù)類必須擁有一個(gè)構(gòu)造方法,該方法至少包含一個(gè)參數(shù),一個(gè)沒(méi)有數(shù)據(jù)的數(shù)據(jù)類是沒(méi)有任何用處的;

? 與普通的類不同,數(shù)據(jù)類構(gòu)造方法的參數(shù)強(qiáng)制使用var或者val進(jìn)行聲明;

? data class之前不能用abstract、open、sealed或者inner進(jìn)行修飾;

? 在Kotlin1.1版本前數(shù)據(jù)類只允許實(shí)現(xiàn)接口,之后的版本既可以實(shí)現(xiàn)接口也可以繼承類。

10. 何謂伴生

引用

顧名思義,“伴生”是相較于一個(gè)類而言的,意為伴隨某個(gè)類的對(duì)象,它屬于這個(gè)類所有,因此伴生對(duì)象跟Java中static修飾效果性質(zhì)一樣,全局只有一個(gè)單例。它需要聲明在類的內(nèi)部,在類被裝載時(shí)會(huì)被初始化。

11. 關(guān)于泛型

引用

關(guān)于協(xié)變

普通方式定義的泛型是不變的,簡(jiǎn)單來(lái)說(shuō)就是不管類型A和類型B是什么關(guān)系,Generic<A>與Generic<B>(其中Generic代表泛型類)都沒(méi)有任何關(guān)系。比如,在Java中String是Oject的子類型,但List<String>并不是List<Object>的子類型,在Kotlin中泛型的原理也是一樣的。但是,Kotlin的List為什么允許List<String>賦值給List<Any>呢?

public interface List<E> extends Collection<E> {
  ...
}
public interface List<out E> : Collection<E> {
  ...
}

關(guān)鍵在于這兩個(gè)List并不是同一種類型。如果在定義的泛型類和泛型方法的泛型參數(shù)前面加上out關(guān)鍵詞,說(shuō)明這個(gè)泛型類及泛型方法是協(xié)變,簡(jiǎn)單來(lái)說(shuō)類型A是類型B的子類型,那么Generic<A>也是Generic<B>的子類型,比如在Kotlin中String是Any的子類型,那么List<String>也是List<Any>的子類型,所以List<String>可以賦值給List<Any>。

List協(xié)變的特點(diǎn)是它將無(wú)法添加元素,只能從里面讀取內(nèi)容。假如支持協(xié)變的List允許插入新對(duì)象,那么它就不再是類型安全的了,也就違背了泛型的初衷。

所以我們可以得出結(jié)論:支持協(xié)變的List只可以讀取,而不可以添加。其實(shí)從out這個(gè)關(guān)鍵詞也可以看出,out就是出的意思,可以理解為L(zhǎng)ist是一個(gè)只讀列表。在Java中也可以聲明泛型協(xié)變,用通配符及泛型上界來(lái)實(shí)現(xiàn)協(xié)變:<? extends Object>,其中Object可以是任意類。

關(guān)于逆變

簡(jiǎn)單來(lái)說(shuō),假若類型A是類型B的子類型,那么Generic<B>反過(guò)來(lái)是Generic<A>的子類型。

前面我們說(shuō)過(guò),用out關(guān)鍵字聲明的泛型參數(shù)類型將不能作為方法的參數(shù)類型,但可以作為方法的返回值類型,而in剛好相反。

interface WirteableList<in T> {
fun get(index: Int): T    //Type parameter T is declared as 'in' but occurs in 'out' position in type T

fun get(index: Int): Any   //允許

fun add(t: T): Int //允許
}

我們不能將泛型參數(shù)類型當(dāng)作方法返回值的類型,但是作為方法的參數(shù)類型沒(méi)有任何限制,其實(shí)從in這個(gè)關(guān)鍵詞也可以看出,in就是入的意思,可以理解為消費(fèi)內(nèi)容,所以我們可以將這個(gè)列表看作一個(gè)可寫、可讀功能受限的列表,獲取的值只能為Any類型。在Java中使用<? super T>可以達(dá)到相同效果。

Kotlin與Java的型變比較


Kotlin與Java的型變比較

關(guān)于通配符

MutableList<*>與MutableList<Any?>不是同一種列表,后者可以添加任意元素,而前者只是通配某一種類型,但是編譯器卻不知道這是一種什么類型,所以它不允許向這個(gè)列表中添加元素,因?yàn)檫@樣會(huì)導(dǎo)致類型不安全。

不過(guò)細(xì)心的讀者應(yīng)該發(fā)現(xiàn)前面所說(shuō)的協(xié)變也是不能添加元素,那么它們兩者之間有什么關(guān)系呢?其實(shí)通配符只是一種語(yǔ)法糖,背后上也是用協(xié)變來(lái)實(shí)現(xiàn)的。所以MutableList<*>本質(zhì)上就是MutableList<out Any?>,使用通配符與協(xié)變有著一樣的特性。

點(diǎn)評(píng)

這一小節(jié)對(duì)于理解泛型的型變有很大的幫助,不過(guò)前提是你需要先理解Java中的PECS原則(Producer Extends Consumer Super),再閱讀下面的協(xié)變和逆變就會(huì)輕松不少,其中的示例代碼好評(píng)。

協(xié)變和逆變描述的就是在集合中,子類與父類之間的轉(zhuǎn)換關(guān)系。協(xié)變即子類集合可賦值給父類集合,逆變即父類集合可賦值給子類集合,這是他們最大的特點(diǎn)。只是由于Java本身泛型的擦除特性,整出了一些副作用,如:協(xié)變不可添加元素,逆變讀取元素不安全;協(xié)變不可作為入?yún)?,逆變不可作為返回值等副作用?/p>

Java泛型是高階知識(shí),對(duì)于開發(fā)框架有很大的幫助,屬于進(jìn)階必備技能。泛型的詳細(xì)知識(shí)可參考 http://www.itdecent.cn/p/716e941b3128 里面的2.12小節(jié)。

12. 關(guān)于惰性求值

引用1

在編程語(yǔ)言理論中,惰性求值(Lazy Evaluation)表示一種在需要時(shí)才進(jìn)行求值的計(jì)算方式。在使用惰性求值的時(shí)候,表達(dá)式不在它被綁定到變量之后就立即求值,而是在該值被取用時(shí)才去求值。通過(guò)這種方式,不僅能得到性能上的提升,還有一個(gè)最重要的好處就是它可以構(gòu)造出一個(gè)無(wú)限的數(shù)據(jù)類型。

通過(guò)上面的定義我們可以簡(jiǎn)單歸納出惰性求值的兩個(gè)好處,一個(gè)是優(yōu)化性能,另一個(gè)就是能夠構(gòu)造出無(wú)限的數(shù)據(jù)類型。

list.asSequence().filter {it > 2}.map {it * 2}.toList()

其實(shí),Kotlin中序列的操作就分為兩類,一類是中間操作,另一類則為末端操作。

引用2 中間操作

在對(duì)普通集合進(jìn)行鏈?zhǔn)讲僮鞯臅r(shí)候,有些操作會(huì)產(chǎn)生中間集合,當(dāng)用這類操作來(lái)對(duì)序列進(jìn)行求值的時(shí)候,它們就被稱為中間操作,比如上面的filter和map。每一次中間操作返回的都是一個(gè)序列,產(chǎn)生的新序列內(nèi)部知道如何去變換原來(lái)序列中的元素。中間操作都是采用惰性求值的

引用3 末端操作

在對(duì)集合進(jìn)行操作的時(shí)候,大部分情況下,我們?cè)谝獾闹皇墙Y(jié)果,而不是中間過(guò)程。末端操作就是一個(gè)返回結(jié)果的操作,它的返回值不能是序列,必須是一個(gè)明確的結(jié)果,比如列表、數(shù)字、對(duì)象等表意明確的結(jié)果。末端操作一般都放在鏈?zhǔn)讲僮鞯哪┪?,在?zhí)行末端操作的時(shí)候,會(huì)去觸發(fā)中間操作的延遲計(jì)算,也就是將“被需要”這個(gè)狀態(tài)打開了。

普通集合在進(jìn)行鏈?zhǔn)讲僮鞯臅r(shí)候會(huì)先在list上調(diào)用filter,然后產(chǎn)生一個(gè)結(jié)果列表,接下來(lái)map就在這個(gè)結(jié)果列表上進(jìn)行操作。而序列則不一樣,序列在執(zhí)行鏈?zhǔn)讲僮鞯臅r(shí)候,會(huì)將所有的操作都應(yīng)用在一個(gè)元素上,也就是說(shuō),第1個(gè)元素執(zhí)行完所有的操作之后,第2個(gè)元素再去執(zhí)行所有的操作,以此類推。

13. 內(nèi)聯(lián)函數(shù)簡(jiǎn)化抽象工廠

引用

何為抽象工廠模式?即為創(chuàng)建一組相關(guān)或相互依賴的對(duì)象提供一個(gè)接口,而且無(wú)須指定它們的具體類。

package factory

interface Computer
class Dell : Computer
class Asus : Computer
class Acer : Computer

abstract class AbstractFactory {
    abstract fun produce(): Computer

    companion object {
        operator fun invoke(factory: AbstractFactory): AbstractFactory {
            return factory
        }
    }
}

class DellFactory : AbstractFactory() {
    override fun produce() = Dell()
}

class AsusFactory : AbstractFactory() {
    override fun produce() = Asus()
}

class AcerFactory : AbstractFactory() {
    override fun produce() = Acer()
}

abstract class AbstractFactory2 {
    abstract fun produce(): Computer

    companion object {
        inline operator fun <reified T : Computer> invoke(): AbstractFactory2 =
                when (T::class) {
                    Dell::class -> DellFactory2()
                    Asus::class -> AsusFactory2()
                    Acer::class -> AcerFactory2()
                    else -> throw IllegalArgumentException()
                }
    }
}

class DellFactory2 : AbstractFactory2() {
    override fun produce() = Dell()
}

class AsusFactory2 : AbstractFactory2() {
    override fun produce() = Asus()
}

class AcerFactory2 : AbstractFactory2() {
    override fun produce() = Acer()
}

fun main() {
    testAbsFactory()
    testAbsFactory2()
}

private fun testAbsFactory2() {
    // Kotlin中的內(nèi)聯(lián)函數(shù)來(lái)改善每次都要傳入工廠類對(duì)象的做法
    val dellFactory = AbstractFactory2<Dell>()
    val dell = dellFactory.produce()
    println(dell)
}

private fun testAbsFactory() {
    // 當(dāng)你每次創(chuàng)建具體的工廠類時(shí),都需要傳入一個(gè)具體的工廠類對(duì)象作為參數(shù)進(jìn)行構(gòu)造,這個(gè)在語(yǔ)法上顯然不是很優(yōu)雅
    val dellFactory = AbstractFactory(DellFactory())
    val dell = dellFactory.produce()
    println(dell)
}

由于Kotlin語(yǔ)法的簡(jiǎn)潔,以上例子的抽象工廠類的設(shè)計(jì)也比較直觀。然而,當(dāng)你每次創(chuàng)建具體的工廠類時(shí)(AbstractFactory),都需要傳入一個(gè)具體的工廠類對(duì)象作為參數(shù)進(jìn)行構(gòu)造,這個(gè)在語(yǔ)法上顯然不是很優(yōu)雅。而AbstractFactory2就是利用Kotlin中的內(nèi)聯(lián)函數(shù)來(lái)改善這一情況。我們所需要做的,就是用inline+reified重新實(shí)現(xiàn)AbstractFactory2類中的invoke方法。

這下我們的AbstractFactory2類中的invoke方法定義的前綴變長(zhǎng)了很多,但是不要害怕,如果你已經(jīng)掌握了內(nèi)聯(lián)函數(shù)的具體應(yīng)用,應(yīng)該會(huì)很容易理解它。我們來(lái)分析下這段代碼:

1)通過(guò)將invoke方法用inline定義為內(nèi)聯(lián)函數(shù),我們就可以引入reified關(guān)鍵字,使用具體化參數(shù)類型的語(yǔ)法特性;

2)要具體化的參數(shù)類型為Computer,在invoke方法中我們通過(guò)判斷它的具體類型,來(lái)返回對(duì)應(yīng)的工廠類對(duì)象。

現(xiàn)在我們終于可以用類似創(chuàng)建一個(gè)泛型類對(duì)象的方式,來(lái)構(gòu)建一個(gè)抽象工廠具體對(duì)象了。不管是工廠方法還是抽象工廠,利用Kotlin的語(yǔ)言特性,我們?cè)谝欢ǔ潭壬细倪M(jìn)、簡(jiǎn)化了Java中設(shè)計(jì)模式的實(shí)現(xiàn)。

點(diǎn)評(píng)

這一節(jié)的知識(shí)點(diǎn)在實(shí)際工作中有很大的用處,inline結(jié)合reified,實(shí)現(xiàn)具體化類型參數(shù)。對(duì)比Java,kt在這塊確實(shí)抗打,代碼寫出來(lái)又進(jìn)一步優(yōu)雅了呢。

14. 構(gòu)建者模式的不足

引用

1)如果業(yè)務(wù)需求的參數(shù)很多,代碼依然會(huì)顯得比較冗長(zhǎng);

2)你可能會(huì)在使用Builder的時(shí)候忘記在最后調(diào)用build方法;

3)由于在創(chuàng)建對(duì)象的時(shí)候,必須先創(chuàng)建它的構(gòu)造器,因此額外增加了多余的開銷,在某些十分注重性能的情況下,可能就存在一定的問(wèn)題。

15. by關(guān)鍵字簡(jiǎn)化裝飾者模式

引用

裝飾者模式,在不必改變?cè)愇募褪褂美^承的情況下,動(dòng)態(tài)地?cái)U(kuò)展一個(gè)對(duì)象的功能。該模式通過(guò)創(chuàng)建一個(gè)包裝對(duì)象,來(lái)包裹真實(shí)的對(duì)象。

總結(jié)來(lái)說(shuō),裝飾者模式做的是以下幾件事情:

? 創(chuàng)建一個(gè)裝飾類,包含一個(gè)需要被裝飾類的實(shí)例;

? 裝飾類重寫所有被裝飾類的方法;

? 在裝飾類中對(duì)需要增強(qiáng)的功能進(jìn)行擴(kuò)展。

可以發(fā)現(xiàn),裝飾者模式很大的優(yōu)勢(shì)在于符合“組合優(yōu)于繼承”的設(shè)計(jì)原則,規(guī)避了某些場(chǎng)景下繼承所帶來(lái)的問(wèn)題。然而,它有時(shí)候也會(huì)顯得比較啰唆,因?yàn)橐貙懰械难b飾對(duì)象方法,所以可能存在大量的樣板代碼。

在Kotlin中,我們可以讓裝飾者模式的實(shí)現(xiàn)變得更加優(yōu)雅。猜想你已經(jīng)想到了它的類委托特性,我們可以利用by關(guān)鍵字,將裝飾類的所有方法委托給一個(gè)被裝飾的類對(duì)象,然后只需覆寫需要裝飾的方法即可。

interface MacBook {
    fun getCost(): Int
    fun getDesc(): String
    fun getProdDate(): String
}

class MacBookPro : MacBook {
    override fun getCost() = 10000
    override fun getDesc() = "Macbook Pro"
    override fun getProdDate() = "Late 2019"
}

class ProcessorUpgradeMacBookPro(private val macBook: MacBook) : MacBook by macBook {
    override fun getCost() = macBook.getCost() + 219
    override fun getDesc() = macBook.getDesc() + ", +1G Memory"
}

fun main() {
    val macBookPro = MacBookPro()
    val processorUpgradeMacBookPro = ProcessorUpgradeMacBookPro(macBookPro)
    println(processorUpgradeMacBookPro.getCost())
    println(processorUpgradeMacBookPro.getDesc())
}

如代碼所示,我們創(chuàng)建一個(gè)代表MacBook Pro的類,它實(shí)現(xiàn)了MacBook的接口的3個(gè)方法,分別表示它的預(yù)算、機(jī)型信息,以及生產(chǎn)的年份。當(dāng)你覺得原裝MacBook的內(nèi)存配置不夠的時(shí)候,希望再加入一條1G的內(nèi)存,這時(shí)候配置信息和預(yù)算方法都會(huì)受到影響。

所以通過(guò)Kotlin的類委托語(yǔ)法,我們實(shí)現(xiàn)了一個(gè)ProcessorUpgradeMacbookPro類,該類會(huì)把MacBook接口所有的方法都委托給構(gòu)造參數(shù)對(duì)象macbook。因此,我們只需通過(guò)覆寫的語(yǔ)法來(lái)重寫需要變更的cost和getDesc方法。由于生產(chǎn)年份是不會(huì)改變的,所以不需重寫,ProcessorUpgradeMacbookPro類會(huì)自動(dòng)調(diào)用裝飾對(duì)象的getProdDate方法。

總的來(lái)說(shuō),Kotlin通過(guò)類委托的方式減少了裝飾者模式中的樣板代碼,否則在不繼承Macbook類的前提下,我們得創(chuàng)建一個(gè)裝飾類和被裝飾類的公共父抽象類。

點(diǎn)評(píng)

裝飾者模式問(wèn)題所在:要重寫所有的裝飾對(duì)象的方法。這也就極大的限制了其使用場(chǎng)景,有時(shí)候還不如繼承來(lái)的實(shí)在。但kt中,通過(guò)by關(guān)鍵字委托給一個(gè)對(duì)象,完全化解了這波尷尬,只能說(shuō)kt語(yǔ)法實(shí)在是高。

彩蛋

看完書以后整理的筆記大綱

筆記大綱
最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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