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