###### tags: `Kotlin` # 初探 Kotlin Kotlin 以俄羅斯聖彼得堡附近的一座島嶼命名,因為開發團隊大部分位於這座島 Kotlin 設計出來是希望能取代 Java,這個語言的目標有三個 第一是必須要是靜態型態、第二完全相容 Java、第三是易於學習與推理 ==目標平台== 主要目標是提供一個更簡潔、更高效、更安全的 Java 替代品 適用於當今使用 Java 的任何環境,除 Java 外,Kotlin 可以被編譯成 JavaScript 使用 Kotlin 最常見的領域是: - 建構伺服器端的程式碼(通常是網頁的後台) - 建構 Android APP :::info Kotlin/JVM 可將 Kotlin 編譯成 Java 字節碼,也就是所謂的 Bytecode,而這種字節碼是專門為 JVM 所設計的,JVM 會將字節碼轉譯為可直接執行的指令,因此 Java 和 Kotlin 本質上是使用相同的語言進行溝通的,這也使得 Kotlin 可以取代 Java。 ::: :::info Kotlin/JS 可將 Kotlin 生成 JavaScript,而目前正在開發 Kotlin/JS IR 編譯器 (處於 Beta),這種編譯器利用了一種新方法,先轉換為Kotlin intermediate representation (IR),隨後編譯為 JavaScript,而不是直接從 Kotlin 生成 JavaScript 代碼。這優化並改進以前編譯器中存在的痛點,例如生成的代碼大小(通過消除死代碼),以及 JavaScript 和 TypeScript 生態系統的互操作性。 > https://kotlinlang.org/docs/js-ir-compiler.html ::: ==靜態型態== Kotlin 跟 Java 一樣是靜態型態的程式語言,這表示程式中的型態,在編譯時都是已知的 編譯器可以驗證你正在擷取的物件上存在的方法和屬性 而動態型態的語言,能儲存、回傳任何型態的變數與函式,並在執行時解析引用的方法和屬性 這大大提升靈活度,但缺點是編譯期間無法檢測拼寫錯誤,可能造成執行階段的錯誤 優點 - 效能:執行速度更快,因為不需要在執行時才確認與檢查 - 可靠性:編譯時就驗證程式的正確性,大幅降低執行時產生崩潰的可能性 - 可維護性:型別確定能提高可讀性,較易於閱讀與維護 - 工具支援:可靠的重構、精確的程式碼實作和其他 IDE 功能 有優點肯定也有缺點,但 Kotlin 支援<font color=tomato>型態推論</font>,所以靜態型態大部分的缺點都消失了 因為不需要明確的宣告型態,可以由上下文進行推論 此外,Kotlin 也支援<font color=tomato>可空型態、函式型態</font> :::info 靜態型態指能在編譯時期確認與檢查型別,動態型態則是在執行時才確認與檢查型別 ::: ==免費及開源== 編譯器、函式庫、其他相關工具都是開源的,而且可免費用於任何目的 ## 函式 它最大的特色是 if 是一個帶有結果值的表示式,如同 Java 的三元運算子(Kotlin 中沒有) ```kotlin= fun max(a: Int, b: Int): Int { return if (a > b) a else b } ``` 上述函式可以再簡化 ```kotlin= fun max(a: Int, b: Int): Int = if (a > b) a else b ``` 如果函式用大括號寫成,可以說此函式有一個<font color=tomato>區塊主體(block body)</font> 如果它直接回傳一個表示式,則它有一個<font color=tomato>表示式主體(expression body)</font> 上述函式因為<font color=tomato>型態推論</font>,所以可以再簡化,直接省略回傳型態的宣告 ```kotlin= fun max(a: Int, b: Int) = if (a > b) a else b ``` ## 變數 有兩種關鍵字可以宣告變數 - var:可變的參考,這個變數的值可以被改變 - val:不變的參考,在初始化後就不能重新指派,對應到 Java 的 final :::info 預設情況下,Kotlin 宣告的所有變數都應盡量使用 val 關鍵字,僅在必要時將其更改為 var ::: ## 字串 在使用變數組合字串的時候,可以使用 $ ```kotlin= val age = 18 println("I am $age years ago") ``` $ 也可以放入表達式 ```kotlin= val age = 18 println("I am ${age + 1} years ago") ``` :::info 使用 $ 比起一般的組合方式效率更高、更緊湊,因為編譯後的程式碼會建立一個 StringBuilder,並為其加入常數(字串)與變數($ 後方的變數) ::: ## 類別與屬性 ==類別== 在 Java 中建立一個類別,如下 ```java= public class Person { private final String name; public Person(String name) { this.name = name; } public String getName() { return name; } } ``` 在 Kotlin 中會變成這樣 ```kotlin= class Person(val name: String) ``` 可以看到建構式、get 方法都消失了,這些都被 Kotlin 省略 而 public 也消失了,因為 Kotlin 中,public 是預設的 這種只有資料,沒有包含程式碼的類別,被稱為<font color=tomato>值物件(Value Objects)</font> ==屬性== 在 Java 中,資料儲存通常是私有欄位,並給予<font color=tomato>存取器方法</font>,而欄位和其存取器的組合被稱為屬性 但在 Kotlin 中,宣告屬性的方式與宣告變數相同,只要宣告了屬性,就同時宣告了對應的存取器 ```kotlin= val person = Person("Joe") println(person.name) //Joe person.name = "Allen" println(person.name) //Allen ``` 如果不想使用 Kotlin 自動產生的存取器或是有其他需求,也可以客製化 以下的方式可以不儲存 isAdult 這個欄位,而是在 get 時,做出計算並回傳 這樣的屬性也被稱作<font color=tomato>計算屬性 (Computed Property)</font> ```kotlin= class Person(val name: String, val age: Int) { val isAdult: Boolean get() { return age >= 18 } } ``` ```kotlin= val person = Person("Joe", 18) println(person.isAdult) //true ``` :::success 重點整理:屬性 (Property) 的定義是 Field + Accessor 也就是欄位(狀態) + 存取器(方法) ::: :::info 這種客製的 getter 屬性與一個沒有參數的函式,哪個比較好呢? 其實兩種都是相似的 實作與效能上沒有區別,只差在可讀性,如果要描述類別的特性,宣告成屬性會比較妥當 ::: > 補充: [屬性僅開放get不開放set的方式](https://stackoverflow.com/questions/33428957/kotlin-public-get-private-set-var) ## 列舉和 when ==列舉== 在 Kotlin 中,enum 是一個軟關鍵字(soft keyword),他在 class 前有特殊含義 但是可以在其他地方作為常規名稱。列舉不是值列表,所以可以在列舉類別上宣告屬性和方法 宣告一個簡單的列舉類別 ```kotlin= enum class Color { RED, BLUE } ``` 宣告含有屬性、方法的列舉類別 ```kotlin= enum class Color(val r: Int, val g: Int, val b: Int) { //列舉常數,最後需加上分號區隔 fun RED(255, 0, 0), BLUE(0, 0, 255); fun rgb() = (r * 256 + g) * 256 + b } ``` ```kotlin= println(Color.BLUE.rgb()) //255 ``` 每一個列舉常數都是一個物件,所以可以定義自己的匿名類別,內部能有各自的方法 也可以覆寫基類的方法,此外每一個列舉常數都擁有兩個屬性: - name:String 類型,可以取得它的名稱 - ordinal:Int 類型,它在列舉類別中宣告的順序,即為索引值 以下使用抽象的概念,讓列舉常數必須實作基類的方法,並打印 name、ordinal ```kotlin= enum class Color(val r: Int, val g: Int, val b: Int) { RED(255, 0, 0) { //定義自己的匿名類別,並實作基類的方法 override fun print() { println(rgb()) } }; fun rgb() = (r * 256 + g) * 256 + b abstract fun print() } ``` ```kotlin= Color.RED.print() //16711680 println(Color.RED.name) //RED println(Color.RED.ordinal) //0 ``` 其他常用的列舉方法: - EnumClass.valueOf(value: String):返回 EnumClass,用 String 取得 EnumClass 對應的常數 - EnumClass.values():返回 Array,裡面放著 EnumClass 中的所有列舉常數 ```kotlin= //定義列舉類別 enum class Color { RED, GREEN, BLUE } //自訂一個返回所有列舉常數名稱的方法 inline fun <reified T : Enum<T>> printAllValues() { println(enumValues<T>().joinToString { it.name }) } ``` ```kotlin= printAllValues<Color>() //RED, GREEN, BLUE println(enumValueOf<Color>("RED")) //RED println(Color.valueOf("GREEN")) //GREEN println(Color.values()[0]) //RED println(Color.values()[1]) //GREEN ``` :::info enumValueOf、enumValues 功用跟 valueOf、values 相同 ::: ==when== when 取代 Java 中的 switch,以下使用 when 與列舉作搭配 ```kotlin= enum class Color { RED, GREEN, BLUE } fun isFavoriteColor(color: Color) = when (color) { Color.RED -> true Color.GREEN -> false Color.BLUE -> false } ``` ```kotlin= print(isFavoriteColor(Color.RED)) //true ``` 以上的 isFavoriteColor 方法,可以用分支結合來簡化 ```kotlin= fun isFavoriteColor(color: Color) = when (color) { Color.RED -> true Color.GREEN, Color.BLUE -> false } ``` when 比 switch 更強大的一點是它允許判斷任何物件,以下使用 set 物件 ```kotlin= enum class Color { RED, GREEN, BLUE, YELLOW, ORANGE } fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) { setOf(Color.RED, Color.YELLOW) -> Color.ORANGE setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN else -> throw Exception("Dirty color") } ``` ```kotlin= print(mix(Color.YELLOW, Color.RED)) //ORANGE ``` :::info set 是集合,最大的特色是內容沒有特定順序,且不重覆,只要包含的內容相同 那麼這個 set 的集合就是相等的,這樣的特性剛好符合混合顏色的功能(上述範例) ::: 上述的 mix 方法,每次都會建立很多的 set,效率相當差,要改善可以用不帶參數的 when 來完成效率較高的判斷,但是程式碼的可讀性會比先前還要差 ```kotlin= fun mix(c1: Color, c2: Color) = when { (c1 == Color.RED && c2 == Color.YELLOW) || (c2 == Color.RED && c1 == Color.YELLOW) -> Color.ORANGE (c1 == Color.YELLOW && c2 == Color.BLUE) || (c2 == Color.YELLOW && c1 == Color.BLUE) -> Color.GREEN else -> throw Exception("Dirty color") } ``` ## while 和 for 迴圈 Kotlin 中有 while、do-while,這與 Java 沒有甚麼差別,所以不特別說明 在這裡先介紹區間與級數 - 區間:兩個值之間的間隔,例如 1..10 是一個區間(range) - 級數:如果可以迭代一個區間的所有值,則此區間稱為級數(progression) 以下介紹 for 迴圈的常用方式 ```kotlin= for (i in 1..10) println(i) //1、2...10 for (i in 1 until 10) println(i) //1、2...9 for (i in 10 downTo 1 step 2) println(i) //10、8...2 ``` ==迭代映射== 將 map(映射) 的 key、value 對應兩個變數 ```kotlin= val map = mutableMapOf<Char, String>() for (c in 'A'..'D') { val binary = Integer.toBinaryString(c.toInt()) map[c] = binary } for ((letter, binary) in map) println("$letter = $binary") ``` ==使用 in 檢查集合、區間成員== in 可以用於檢查集合、區間的成員 ```kotlin= fun isLetter(c: Char) = when (c) { in 'a'..'z' -> "Yes, it is lower" in 'A'..'Z' -> "Yes, it is upper" else -> "No" } println("Kotlin" in setOf("Java", "Kotlin")) //true ``` ## 例外 跟 Java 的例外處理相似,使用後若有例外則可以捕獲並處理 若無使用且發生例外,則此例外就會重新進入堆疊 ```kotlin= val percentage = if (num in 0..100) num else throw IllegalArgumentException("num must between 0 and 100") ``` try、catch、finally 跟 Java 不同之處在於 throws 子句不存在了 ```kotlin= fun readNum(reader: BufferedReader): Int? { try { val line = reader.readLine() return Integer.parseInt(line) } catch (e: NumberFormatException) { return null } finally { reader.close() } } ``` try 也可以作為表達式 ```kotlin= fun readNum(reader: BufferedReader) { val num = try { Integer.parseInt(reader.readLine()) } catch (e: NumberFormatException) { null } println(num) } ``` ## 參考文章 [列舉](http://www.liying-cn.net/kotlin/docs/reference/enum-classes.html) [List、Map、Set 集合](http://tw-hkt.blogspot.com/2018/11/kotlin-listof.html) > 推薦書籍:Kotlin 實戰手冊 Dmitry Jemerov Svetlana Isakova 著/蔡明志 譯 ## 未完事項 - typealias - 中綴表示法、中綴函數 infix