[Day 03] 遠征 Kotlin × 變數型別

在任何一種程式語言都有資料型別介紹,而此篇我們將來了解 Kotlin 在資料型別上的特性、操作、轉換等內容。

在 Kotlin 官方文件中有提到:

In Kotlin, everything is an object in the sense that we can call member functions and properties on any variable.

上述內容得知,Kotlin 的任何東西都是一個物件,可以存取任何對象的相關方法與屬性,不像 Java 有區分原始型別(Primitive Type)參考型態(Reference Types),在開發上有時候甚至需要做轉換才可使用。而 Kotlin 在宣告變數時使用的是靜態類型系統(static type system),即編輯器會按照變數類型辨識程式碼,判斷是否有存在類型與數值不符合的狀況發生,若有出現,編輯器會立即指出,例如下圖提示訊息:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179s0fDvpLeBE.png

變數宣告

Kotlin 在變數宣告時主要會使用到兩種關鍵字 valvar

  • val 用於唯讀變數,一旦給值就無法再修改
  • var 用於需要重新修改數值的情況
1
2
3
4
5
fun main() {
val readOnlyVariable = "鐵人賽第十二屆" // 宣告一個唯讀變數
var playerName = "選手一號" // 宣告一個可重新修改數值的變數
playerName = "選手二號" // 重新賦予新數值
}

Kotlin 官方這邊也有建議開發者在開發上建議優先使用 val,當遇到需要修改數值時再轉為 var 即可,若使用 var 宣告變數,開發者若沒有在程式中修改過,Intellij 編輯器也會提示建議改為 val,如下圖:
https://ithelp.ithome.com.tw/upload/images/20200912/20121179ihruzGQCee.png

空值型態

還記得嗎?我們在上一章有提到 Kotlin 有一個優勢是可以避免以前 Java 開發中常見的 NullPointerException 情況發生,主要原因是因為 Kotlin 預設宣告都只能是非 null 型態,例如以下範例,當我們想要進行指派 null 值給 String 時會發生編譯錯誤狀況:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179hI5ojOeMew.png

這樣的錯誤檢查就能夠避免開發者經常會有出現錯誤的問題,而如果在開發情境上確實有必要使用 null 值,則可以將變數定義為 nullable 狀態,即在變數的型態定義上加上 ? 即可,如下範例:

1
2
3
4
5
fun main() {
var test: String? = "鐵人賽"
test = null
println(test) // 印出 null
}

型別判斷處理

在介紹基本型別前,先介紹 Kotlin 在變數上有個特色是型別判斷處理,可對於已指派預設值的宣告變數自動定義型別,允許開發者省略型別定義,以下我們嘗試宣告一個變數,並輸出該變數的型別來看 Kotlin 是否有自動幫我們進行型別宣告,如下範例:

此範例先宣告變數 name 為「鐵人賽」,再利用「::class.simpleName」印出變數型別結果為 String

1
2
3
4
fun main() {
val name = "鐵人賽"
println(name::class.simpleName) // 印出 String -> 代表 Kotlin 自動幫我們定義型態
}

資料型別

Kotlin 在資料型別與 Java 非常相似,只差在變數型態必須使用首字大寫,型別分別如下:

  • 數值型別 Numbers (種類可依長度區分)

    • Byte (8 Bits)

    • Short (16 Bits)

    • Int (32 Bits)

    • Long (64 Bits)

    • Float (32 Bits)

    • Double (64 Bits)

      數值變數在操作上可直接宣告型態或是透過型別判斷進行操作:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      fun main() {
      val byte: Byte = 1
      val short: Short = 2
      val int: Int = 3
      val long: Long = 4L
      val float: Float = 5f
      val double: Double = 6.0

      println("Byte => $byte")
      println("Short => $short")
      println("Int => $int")
      println("Long => $long")
      println("Float => $float")
      println("Double => $double")
      }

      前面有提到 Kotlin 的一切都是物件,在以前 Java 變數型態有分為基本型別(Primitive type)參考型別(Reference type),即 intInteger 的差別,而在 J2SE 5.0 時有提供自動裝箱(autoboxing)拆箱(unboxing)來進行包裹基本型態,但在 Kotlin 中,只存在數值的裝箱,不存在拆箱,因為 Kotlin 是沒有存在基本資料型態的,下面將示範如何進行裝箱操作:

      此範例操作須搭配上面提到的概念-空值型態達成裝箱效果,會發現裝箱前與裝箱後的數值都一樣

      1
      2
      3
      4
      5
      6
      fun main() {
      val number: Int = 913
      val numberInBox: Int? = number
      println("裝箱前數值: $number , 裝箱後數值: $numberInBox")
      // 裝箱前數值: 913 , 裝箱後數值: 913
      }

      上面範例我們會發現兩個數值印出來雖然是相等的,但其實在 Kotlin 判斷數值是否相等有兩種比較方式(=====),== 是判斷數值是否相等, === 則是判斷兩個數值在記憶體位置是否相等,而其實 Kotlin 在變數裝箱操作時,記憶體位置會根據其資料型別的數值範圍進行定義,我們可以利用下面範例進行示範:

      我們會發現當 a 變數為 127 時,判斷兩個裝箱變數會為 true,因為 Int 型態定義數值範圍為 -128 ~ 127,當 b 變數超過 127 數值時,Kotlin 在記憶體分配上會有不同位置狀況發生。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      fun main() {
      val a: Int = 127
      val boxedA: Int? = a
      val anotherBoxedA: Int? = a

      val b: Int = 128
      val boxedB: Int? = b
      val anotherBoxedB: Int? = b

      println(boxedA === anotherBoxedA) // true
      println(boxedB === anotherBoxedB) // false
      }

      Kotlin 在數值轉換上有分顯性轉換與隱性轉換,隱性轉換即 Kotlin 會自動幫我們進行轉換,但若兩個數值為不同型態時,會自動以定義數值範圍較大的型態為轉換後的最終型態,例如以下範例:

      此範例為兩數相加,999為 Long 型態,1為 Int 型態,兩數相加後的結果 number 為 Long 型態

      1
      2
      3
      4
      fun main() {
      val number = 999L + 1
      println(number::class.simpleName) // 印出資料型別為 Long
      }

      而為了避免隱性轉換時自動選擇型態問題,我們在開發上可使用顯性轉換方式,即下面範例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      fun main() {
      val number: Int = 65
      println(number.toByte()) // 印出 65
      println(number.toShort()) // 印出 65
      println(number.toLong()) // 印出 65
      println(number.toFloat()) // 印出 65.0
      println(number.toDouble()) // 印出 65.0
      println(number.toChar()) // 印出 A
      println(number.toString()) // 印出 65
      }
  • 字元型別 Char

    Char 表示字元類型,字元變數必須使用單引號(‘’)表示,在轉換上可利用顯性轉換為數字型態,如以下範例:

    1
    2
    3
    4
    fun main() {
    val char: Char = 'A'
    println(char.toInt()) // 印出 65
    }
  • 字串型別 String

    String 表示字串類型,在輸出時可使用字串模板表示式處理字串組成,再進行輸出,如下範例:

    1
    2
    3
    4
    fun main() {
    val username: String = "Devin"
    println("第十二屆鐵人賽 參加者 $username") // 印出「第十二屆鐵人賽 參加者 Devin」
    }
  • 布林型別 Boolean

    Boolean 表示為布林類型,其值有 truefalse

    1
    2
    3
    4
    5
    6
    fun main() {
    val isFalse: Boolean = false
    val isTrue: Boolean = true

    println(isFalse && isTrue) // 印出「false」
    }
  • 陣列型別 Array<T>

    Kotlin 的 Array 型別在宣告上是以 Array<T> 表示,我們可以到 Kotlin 的 Array 型態定義查看,會發現原始型態已經幫我們定義 get、set、size 與 iterator 方法:

    https://ithelp.ithome.com.tw/upload/images/20200912/20121179oKLvyrROGy.png

    故我們在 Array 操作上可以如下範例進行操作:

    1
    2
    3
    4
    fun main() {
    val data: Array<Int> = arrayOf(1,2,3,4,5) // 宣告Array並賦予 1-5 數值
    data.forEach { println(it) } // 利用 forEach 分別印出數值
    }

Const 作用

在前述有提到唯讀變數 val 不允許重新設定數值,但其實 val 是在程式執行階段(Run time)才進行賦值(Assign Value)動作,而我們若要限制程式在編輯階段(Compile time)就進行賦值動作,應使用 const 關鍵字搭配 val 進行變數宣告,我們可用一個範例來說明 constval 的差異:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179O9kWjS1nt1.png

透過上面範例我們會發現兩件事:

  1. normalVariable 可利用 getRandomValue() 隨機取得 1 - 6 數值,表示程式是先在執行階段利用 getRandomValue() 方法取得數值後,才對 normalVariable 進行賦值
  2. 當我們嘗試將 constVariableFromGetValue 賦予 getRandomValue 方法時,會出現 const val 只能接受常數(constant value)

型別檢測與轉換

  • is 運算子

    is運算子可檢查物件或變數是否屬於某資料型別,如Int、String等,類似於Java的 instanceof

    1
    2
    3
    4
    5
    fun main() {
    val data = "abc"
    println(data is String); // 印出 true
    println(data is Any); // 印出 true
    }
  • as 運算子進行型別轉換

    as運算子用於型別轉換,若要轉換的數值與指定型別相容,轉換就會成功;如果型別不相容,使用 as? 運算子就會返回值null,如下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    fun main() {
    val x: Int = 2
    val y: Int = x as Int
    val z: String? = y as? String

    println(y) // 印出 2
    println(z) // 印出 null
    }

特殊型別

除了上述基本型別以外,Kotlin 還有一些特殊型別運用於物件或函數上,這邊會先進行簡單介紹,會在後續章節介紹時會再深入說明:

1. Any 型別

根據 Kotlin 官方文件所述:

The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.

在此篇文章一開始介紹說明,Kotlin的一切都是物件,而每個物件其實都是繼承 Any 這個型別,此型別相當於Java的 Object 型別,而此型別也可再細分為 Any 與 Any?,Any屬於非空型別的根物件,Any?屬於可空型別的根物件。

2. Unit 型別

在 Java 中,當我們所設計的 function 不需回傳值時,我們會使用到 void 型別,而在 Kotlin 可使用 Unit 型別代替,而且若我們不特地為 function 設定回傳型態時,Kotlin 會自動幫我們預設型態為 Unit 型別,會返回 Unit 型別,例如以下範例。

1
2
3
4
5
6
7
8
fun main() {
val username = getUserName()
println(username::class.simpleName) // 印出 Unit 型別
}

fun getUserName() {

}

3. Nothing 型別

Nothing 型別其實類似於 Unit,Nothing 型別也是不返回任何東西,但差別在於 Nothing 型別意味著此函數不可能成功執行完成,只會拋出異常或是再也回不去函數呼叫的地方。

而 Nothing? 型別則會有一個使用情境,在 Java 中,void不能是變數的型別。也不能被當數值列印輸出。但是,在Java中有個包裝類Void是 void 的自動裝箱型別,如果我們想讓 function 返回型別永遠是 null 的話,可以把返回型別置為這個大寫的V的Void型別,而 Void 即對應 Kotlin 中的 Nothing? 型別。

範例(1) 使用 Nothing 型別

1
2
3
4
5
6
7
fun main() {
getUserName() // 使用 Nothing 型別
}

fun getUserName(): Nothing {
throw NotImplementedError() // 丟出異常
}

範例(2) 使用 Nothing? 型別

1
2
3
4
5
6
7
fun main() {
getUserName() // 使用 Nothing? 型別
}

fun getUserName(): Nothing? {
return null // 保持回傳 null
}

Kotlin 轉換 Java Code

有時候我們可能會好奇在 Kotlin 所撰寫的程式,實際轉換為 Java 會是怎麼樣的語法,此時我們可以利用 intellij 內建的工具進行轉換觀察。

在 Intellij 連續按 Shift 鍵兩次,搜尋「show kotlin」關鍵字,選擇「Show Kotlin Bytecode」,會出現Kotlin位元組碼工具視窗,再點擊「Decompile」按鈕即可觀看轉譯的Java 程式碼。

https://ithelp.ithome.com.tw/upload/images/20200912/20121179AFv0fulGrw.png

Reference