###### 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