上篇已提(tu)到(cao)Java中的各種坑。習慣了C#的各種特性和文法糖後,再轉到Java感覺比較别扭。最後本着反正Java也不是很熟悉,幹脆再折騰折騰其他語言的破罐子破摔的心态,逛了一圈JVM語言,最終決定轉Kotlin。
項目遭遇人員變動,包括我在内就剩兩個人開發,轉型成本低,代碼品質容易控制。
JVM語言。号稱與Java 100%相容。實際使用的确能夠與Java幾乎無縫地互相調用,基本上可以無縫遷移,完美相容Java生态。
OOP。目前OOP仍是主流,友善後續交接或者其它新加入的開發成員上手。
靜态類型。在選擇語言的時候也考慮過像Groovy,JRuby等的動态類型語言。然而俗話說得好,動态一時爽,重構火葬場。當項目變大的時候,靜态類型支援的較為完善的語義分析能夠幫助項目快速整理、重構代碼。并且引入很多函數式特性後,靜态類型語言的開發效率與爽感,不比動态類型語言低多少。
吸收了一些函數式特性。除了常見的lambda,map,filter,reduce之外,還吸收了ruby的一些如對象上下文切換、代碼塊文法糖等便捷的特性(但是也可能導緻代碼可讀性下降)。
對JetBrain的信任。JetBrain在靜态分析的成果上有目共睹。相信JetBrain設計的語言應該會比較有品位(然而嚴格得不近人情的null safety是有點讓人糾結)。
最後,就是剛好看到Kotlin,确認了眼神……
犧牲了CE使得Lambda不像Java中那麼多的限制。引入類似Ruby代碼塊的寫法(預設it參數),讓代碼看起來比較好看,雖然我個人不是很喜歡這種預設約定,但是用起來真香。
不同于其他語言,Kotlin裡的if else,try catch等都是表達式,我們可以直接這樣子寫代碼:
Lambda是最後一個參數時,可以寫在括号外面(學自ruby)。主要是用來讓回調比較好看,和實作DSL。
Receiver。Kotlin不僅有純函數類型,還可以通過Receiver聲明類的方法類型。這個特性可以用來實作類的方法擴充、this切換的功能。
下面代碼給<code>Int</code>擴充了個<code>double</code>方法:
下面例子通過切換<code>this</code>實作了一個類似C#初始化對象的方法:
很多好用的方法,像<code>listOf</code>,<code>mapOf</code>。<code>to</code>操作符等
……
Kotlin中Class預設都是不能繼承的。需要繼承的Class要在聲明的地方加上open修飾。另外提一下有個插件叫all-open,專門用來讓所有Kotlin的類變為可繼承的……
Kotlin不支援可繼承的注解。
<code>List</code>,<code>Map</code>不能修改其内部存儲的元素。需要修改應該用<code>MutableList</code>和<code>MutableMap</code>。
号稱和Java 100%相容,但是不能通路Lombok生成的方法!
因為Lombok的方法是編譯期通過注解處理器(annotation processing)生成的,Kotlin編譯時隻調用了Javac,是以無法處理Lombok定義的方法。強制先編譯Java代碼,後編譯Kotlin代碼,可以解決這個問題,但是又會有新的問題:你不能在Java代碼中調用Kotlin代碼。是以如果你要混合使用Java和Kotlin的話,推薦所有資料類型都用Kotlin寫。
<code>var</code>就是普通變量。<code>val</code>相當于<code>const</code>。平時盡量使用<code>val</code>,有益身心健康。
Null safety是Kotlin宣傳得最多的特性,但是我并沒有放在“好用的特性”節中介紹,因為它的坑非常多,以至于我十分懷疑null safety的好處是否能抵消它帶來的副作用。
所有類型預設都不包括<code>null</code>值,除非加個問号定義為Nullable類型。Nullable類型取值時,強制check null。如果調用Java代碼,預設Java代碼都是Nullable。不過從Java來的變量不做check null倒是不會報error,隻報warning。如果運作時值為<code>null</code>的話,仍然會抛<code>NullPointerException</code>。Kotlin的null safety的特性其實隻是一個編譯器的特性,通過将<code>null</code>與其他類型區分開來,在類型檢查的時候順便檢查了可能出現的<code>NullPointerException</code>,但是在運作時非Nullable的變量實際上也是可以放進去<code>null</code>值的(比如通過反射)。
由于非Nullable類型不被指派為<code>null</code>值(廢話),導緻這些類型的變量可能會沒有預設值!這是個嚴重的問題。如果是像<code>Int</code>,<code>String</code>這種比較像值的類型(其實也是引用類型)還好,可以有<code>0</code>,空字元串等預設值。而像自定義的類,這種類型的變量其實是個引用,如果不能預設為<code>null</code>的話,那麼它的預設值的取值隻能有這麼幾種方案:
類似C語言,未初始化的随機值:會産生更大更不确定硬隐蔽的問題。
定義一個“未初始化”的值:那麼這個值和<code>null</code>有什麼差別?又繞回來了。
類似C++,預設建立一個空對象:但是并非所有類都有預設構造函數,而且在擁有GC的語言中,建立空對象需要配置設定記憶體,還會調用構造函數中的邏輯。聲明變量時引入這麼多過程是非常不合适的。
是以,Kotlin最終選了一種簡單粗暴的方案:禁止變量未初始化。
禁止變量未初始化的問題在于,當你需要定義大量的資料類的時候,你就知道有多蛋疼了——所有屬性都必須有個初始值。這不僅需要多敲不少鍵盤,影響手指健康,當碰到屬性是非Nullable的聚合時,也常常無法确定其初始值。我已經隐隐看到某些開發人員将所有變量都标記為Nullable的畫面了……Kotlin自身也發現了這個問題,是以引入了<code>lateinit</code>特性,然而用起來仍然有點令人膽戰心驚。
反序列化。即使是業務邏輯上明确了不會為<code>null</code>值的屬性,你也無法保證網絡上/資料庫裡傳輸過來的資料中,對應的屬性會不會是<code>null</code>值,或者幹脆漏了,是以就算model設計正确的,實際運作時可能還是會出現<code>NullPointerException</code>。我又隐約看到某些開發人員将所有變量都标記為Nullable的畫面了……另外反序列化時,需要先生成一個空對象,也就是屬性都沒初始化的對象。當然Kotlin不會允許這麼做的,是以還需要引入NoArg插件來自動生成無參數的構造函數……
為了和Java 100%相容,Kotlin不得不跟着Java用類型擦除式泛型,也擁有了前面說過的類型擦除式泛型的所有坑。不過Kotlin可以使用内聯函數來稍微緩解類型擦除的負面影響。比如可以這樣定義json反序列化的方法:
<code>inline fun <reified T> parse(json: String): T = objectMapper.readValue(json, T::class.java)</code>
Kotlin有兩種方法定義一個匿名函數:lambda和anonymous function。當在這兩種方法的函數體中使用return時,執行的語義是不同的。根據官方文檔<code>return</code>會跳出最近的顯示聲明的函數或anonymous function。例如下面的<code>return</code>會直接跳出<code>foo</code>函數。
而下面這個隻是當<code>value == 3</code>時跳過一次循環,相當于其他語言的<code>continue</code>
或者也可以使用Label來指定執行<code>return</code>後跳到的位置(感覺像goto似的)。
另外,break和continue也是有類似的問題。
最近家庭工作都比較忙,這短短的一篇轉型踩坑記竟然寫了個跨年。有些踩坑的記憶随着時間流逝以及用習慣了給慢慢淡化掉了,于是也沒寫進來。目前Java系這邊的開發我盡量使用Kotlin,并沒有碰到什麼根本上的大問題,與Java的相容性也挺好的,有精力的同學可以放心品嘗。