零基礎學習Java之Kotlin從入門到精通

零基礎學習Java之Kotlin從入門到精通

  學習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也是有類似的問題。

文章來自:https://www.itjmd.com/news/show-5343.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章