
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í)在是高。
彩蛋
看完書以后整理的筆記大綱


