Kotlin 實戰範例 (2) 基礎 (變數、型別)

Kotlin 實戰範例

此篇要來介紹在 Kotlin 中如何宣告變數,型別系統又是如何有別於 Java,還有 Kotlin 的程式起點和檔案命名和 Java 有什麼不同。

2.1 Kotlin 的檔案名稱

在 Java 中,檔案名稱必須和類別名稱相同,一個 class App {} 類別,檔案名稱會是 App.java,在 Kotlin 中沒有這個規定,類別名稱和檔案名稱無關,但是我們通常還是會使用一樣的名稱。

Kotlin 檔案的副檔名為 .kt

Kotlin 檔案被編譯後,會以檔名加上 Kt (大寫 K) 做為編譯後的名稱,例如 App.kt 編譯後的檔案為 AppKt.class ,要在 Java 中使用 Kotlin 的程式碼,必須注意名稱是否正確。如果我們想自訂名稱,可以在檔案最上方加入 @file:JvmName("App") 這樣就會指示編譯器將檔案編譯為 App.class

2.2 程式的起點

Kotlin 以 main() 函式做為程式起點,意思是當程式被執行時,會先找到這個 main() 函式來執行,一個應用程式中只會有一個 main() 函式,它是唯一的。

Kotlin 將函式視為第一等級,和類別 (class) 同等地位,因此 main() 函式不需要寫在任何類別之中,這點和 Java 非常不同,一個最基本的 Kotlin 程式如下:

// 接受參數
fun main(args: Array<String>) {
  // ...
}

// 無參數
fun main() {
  // ...
}

對比 Java,它的程式進入點在類別中的靜態 main() 方法,一個最基本的 Java 程式如下:

/* Java */
public class App {
  public static void main(String[] args) {
    // ...
  }
}

對比之下,Kotlin 顯然精簡多了。

main() 函式中的參數是用來接收執行程式時輸入的資料,我們可以來動手試試看,內容如下:

fun main(args: Array<String>) {
  println("Hello, Kotlin ~")
  args.forEach(::println)
}

這樣當我們在執行程式時輸入參數的話,就會被顯示出來。那要在哪裡輸入參數呢?我們可以在命令列中加入,請看範例:

# 在專案根目錄
gradle run --args="Tony Tom Tiffany"
# ------ 輸出結果 ------
Hello, Kotlin ~
Tony
Tom
Tiffany

2.3 放開那個分號

寫 Java 時,你有想過每天寫程式時要浪費多少時間在按下分號 ; 這個按鍵嗎?Kotlin 讓我們省下這個動作,不需要在敍述的結尾加上分號,但是加上分號也不會發生錯誤。如果我們想將兩個敍述寫在同一行,那就一定要用分號來分隔,但是這種寫法不是個好習慣。少了結尾分號的甘擾,在閱讀大量程式碼時也會比較輕鬆些。

2.4 變數的宣告

Kotlin 將變數細分為唯讀 (Read-only 或稱為 immutable 不可變動的) 及可變動 (mutable) 兩種,可變動的變數就是我們一般認知的變數,可以在宣告後任意更動其中的值;唯讀 (不可變動的) 變數則是在宣告時就同時給值,並且無法再次更改,其行為很像常數,但在實作機制及用途上不同。

要宣告唯讀變數使用 val 關鍵字;可變動的變數使用 var 關鍵字。請看範例:

// 宣告唯讀變數
val name = "Tony"
// name = "Tom" // <-- 發生錯誤,無法修改唯讀變數

// 宣告可變動變數
var age = 5
age = 7 // <-- 沒問題

要怎麼選擇使用 valvar 呢?在任何情況下,我們應該優先使用 val,它能避免資料無意間被修改,減少犯錯的機會,只有當我們真正需要時才使用 var,大多數的情況下是不需要用到 var 的。

這裡要提醒一下,Java 10 也新增了 var 關鍵字,這個特性的規範為 JEP 286: Local-Variable Type Inference ,Oracle 也意識到宣告變數時,對於重覆指定資料型別在開發上影響效率,於是新增了區域變數自動推斷型別的機制,雖然名稱和 Kotlin 的 var 一樣,但是它們的概念是不一樣。Kotlin 的型別推斷是內建的,到處都能用。

2.5 資料型別

型別系統 (Type System) 會定義資料型別 (Data Type,或稱資料類型、資料型態,簡稱型別),是用來約束資料屬於哪一種類型的機制。限制變數存放的資料類型,能加強安全性、讓編譯器最佳化,並且增加可讀性,也能讓系統依不同的資料類型而分配大小不等的記憶體空間。

2.5.1 型別指定

當我們宣告一個變數時必須同時指定其資料型別,編譯器可以依此型別資訊,檢查資料是否符合此型別。Kotlin 指定資料型別的方式和 Java 有些不同,它是在變數後方加上冒號 : 在指定資料型別,請看範例:

/* Kotlin */
val a: Int = 1
var b: String = "Tony"

來比較看看 Java 的例子:

/* Java */
int a = 1;
String b = "Tony";

2.5.2 型別推斷

Kotlin 對型別系統加入了型別推斷機制,也就是當我們可以從資料 (變數的值) 得知該變數的型別時,就可以省略型別不寫,編譯器會以資料來反推該變數的型別,請看範例:

val a = 1 // 型別推斷為 Int
var b = "Tony" // 型別推斷為 String

這個例子中的變數其型別和前面例子中是一樣的,但是我們可以省略不寫。

在無法推斷出型別的情況下,就必須在宣告變數時加上型別,例如:

var b: String

因為我們宣告變數時並沒有給值,編譯器將無從得知該變數的型別,因此我們必須指定其型別。

如果讀者寫過腳本語言像是 Javascript,可能有時候會這麼做:

a = 1; // a 現在是整數型別
a = "Tony"; // a 現在變成字串型別
console.log(a);

切記在 Kotlin 是不能這麼做的,靜態型別的意思就是,一經指定型別,該變數的型別就無法動態更改,指派新的值也必需符合資料型別。

2.5.3 基本型別

在 Kotlin 中,任何東西都是物件,所以我們可以存取任何資料所屬型別的相關方法及屬性。

在 Java 中,原始型別 (primitive type) 代表的是原始值,例如數字 1 ,它並不是物件,因此我們不能像使用物件一樣存取它的屬性或呼叫方法;Kotlin 內建了許多和原始值相對應的類別,例如整數對應到 Int 類別,Kotlin 將這些原始型別升級為物件,因此我們可以像是使用物件一樣來使用這些原始型別。

數字 (numbers)、字元 (characters) 和布林值 (booleans) 這類原始值相對應的類別,在執行時期會被表示為原始值 (primitive values),因為它們本來就只是單純的值;但是經過 Kotlin 的設計,對開發者來說這些原始值看起來就像一個普通的物件。

Kotlin 和 Java 最大的不同之處在於,Java 使用 autoboxing (把原始值包成物件) 及 unboxing (將物件還原為原始值) 的機制來處理原始值的物件化機制,開發者必須在需要時將原始值包裝成物件,且在取值時將其解包裝,舉例如下:

/* Java */
// 因為 int 是原始型別無法當成資料型別來使用,所以以下這行無法編譯
// List<int> numbers = new ArrayList<>();
// 必須使用 int 相對應的資料型別 Integer 才可以
List<Integer> numbers = new ArrayList<>();

// 建立 10 個數字加入 numbers 中,
IntStream.range(0, 10).forEach(i -> numbers.add(i));
// 這裡的 i 在加入集合的同時,被 autoboxing 成 Integer
// 如果要自己手動打包,作法是 numbers.add(Integer.valueOf(i))

// 當我們之後要取出 numbers 中的項目來做原始值相關的行為,例如相加時,它會自動 unboxing 為 int,手動解包裝的作法 numbers.get(1).intValue()

Java 當初設計 autoboxing 及 unboxing 機制就是為了簡化這個轉換過程,讀者應該難以想像在這個機制出現之前,我們必須手動把原始值包裝成物件,然後在必須做運算時解包裝成原始值,如果有必要又必須包裝回物件,這繁鎖的過程實在是非常影響開發效率。

如果是 Kotlin 來做:

// Kotlin 只有一種 Int 的型別,沒有原始型別可用
val numbers = ArrayList<Int>()
// 我們不需要做任何型別轉換的動作
(0 until 10).forEach { numbers.add(it) }

Kotlin 在設計的時候,就將所有 Java 中的原始型別都建立相對應的參考型別 (類別),讓我們不必費心去考慮轉換的動作。但是要知道,在執行運算時它仍是以原始值在操作,只是 Kotlin 設計的機制幫我們處理掉了。使用原始值的原因是效率,原始值在計算上會比物件的速度快。

2.5.3.1 數字型別

Kotlin 提供了 6 個內建的數字型別:

TypeBit width
Double64
Float32
Long64
Int32
Short16
Byte8

這些型別的父型別是 Number

注意!這些型別名稱都是首字母大寫;另外,在 Java 中,字元型別 (char) 被歸類為 16 bits 的數字型別,但 Kotlin 沒有把字元型別當成數字型別。

字面常數
  • 10 進制的整數寫法:123
  • 如果要表示為 Long 型別,可以在尾部加大寫 L123L
  • 16 進制的寫法:0x0F
  • 2 進制的寫法:0b00001011
  • Kotlin 不支援 8 進制的寫法
  • Kotlin 的小數預設使用雙精確度浮點數 Double123.5, 123.5e10
  • 要強制使用單精確度浮點數 Float,可以在數字尾部加上大寫 F 或小寫 f123.5F123.5f
  • 可以使用底線 _ 表示法讓數字更易閱讀:1_000_000 表示 100 萬
數字型別的轉型

靜態型別系統在指定資料型別後就無法再更動,如果想將該資料指定給另一種型別,就必須「轉換型別」。

Kotlin 的型別系統非常嚴格,我們先來看看 Java 的例子以比較兩者的不同。在 Java 中,有兩種轉換型別的方法,一個是隱含 (implicit) 轉換,或稱自動轉換,另一種是明確 (explicit) 轉換,請看範例:

/* Java */
int a = 1;
long b = a; // 隱含轉換
long c = (long)a; // 明確轉換

Kotlin 和 Java 非常不同的一點,就是它在設計上沒有隱含 (implicit) 轉換的機制,全部的型別轉換都必須明確指定,請看範例:

/* Kotlin */
val a: Int = 1
// val b: Long = a // 型別錯誤
val b: Long = a.toLong() // 明確轉換

我們看到 Kotlin 非常的嚴格,當我們想把 Int 型別的變數指派給 Long 型別的變數時,就會發生型別錯誤,我們必須明確地使用 toLong() 方法把資料型別轉換成 Long

Kotlin 只有在一個條件下會自動做數字型別的轉換,並且只能從範圍較小的轉成範圍較大的,就是從整個運算式的脈絡裡,能夠明確地判斷轉換成另一個數字型別是可行的,它就會自動轉換,例如:

val a: Long = 1L
// Long + Int 會自動轉換為範圍較大的 Long
val b: Long = a + 2
// 如果沒有自動轉換機制,必須寫成這樣,將會很麻煩
val c: Long = a + 2.toLong()

// 如果想要相加後仍為 Int,就必須明確轉型
val d: Int = a.toInt() + 2
// 或
val e: Int = (a + 2).toInt()

運算符號的成員方法

Kotlin 針對數字的運算符號,實作了一系列有關數字運算的成員方法,所以我們可以呼叫該方法來做運算,例如:

// 以下兩個運算式是相同的
val g = (((10 * 10) + 20) / 3) - 5
val h = 10.times(10).plus(20).div(3).minus(5)
println(g == h) // [結果] true

2.5.3.2 字元型別

字元的資料型別是 Char,以單引號來表示,例如 'a',並且不能被當成數字。Java 開發者請注意,在 Kotlin 中不能把字元拿來和數字比較,像這樣:

val a = 'a'
if (a == 97) { // 錯誤
  // ...
}

// 必須這樣寫
if (a.toInt() == 97) {
  // ...
}

特殊字元可以使用跳脫符號 (倒斜線) 來表示,例如:\t\b\n\r\'\"\\\$,或是 Unicode 字元 '\u0041'

2.5.3.3 布林型別

布林型別 Boolean 只有兩種值:代表「真」的 true 或「假」的 false ,兩個其中一個。

2.5.3.4 陣列型別

Kotlin 為陣列設計了一個相對應的 Array 類別,它有取值的 get() 方法和給值的 set() 方法,並且這兩個方法被一對方括號 [] 重載,所以我們可以使用方括號來操作取值及給值的動作。如果要取得陣列的大小可以讀取 size 屬性。

Kotlin 的標準庫裡提供了一些工具函式方便我們建立陣列,請看以下陣列的使用範例:

// 方法一
val names: Array<String> = arrayOf<String>("Tony", "Andy", "Bob")
// 上面這個是最完整的寫法,但在提供初始值的情況下,可以省略其資料型別,如下:
val names = arrayOf("Tony", "Andy", "Bob")
// 如果沒有初始值就必須提供資料型別,如下:
val names = arrayOf<String>()

// 方法二
val numbers: IntArray = intArrayOf(100, 200, 300)
/* Kotlin 針對數字型別、布林型別、字元型別,提供相對應的工具函式 byteArrayOf()
shortArrayOf()
intArrayOf()
longArrayOf()
floatArrayOf()
doubleArrayOf()
booleanArrayOf()
charArrayOf()
*/

// 方法三
// 建立一個固定容量的陣列,內容為空白字串
val items: Array<String> = Array(3) { "" }
// 接著使用方括號存取值
items[0] = "Book"
println(items[0])

// 相同的動作使用 set 及 get 方法
items.set(1, "Pen")
println(items.get(1))

Kotlin 建議使用方括號來存取陣列的項目值。標準庫中提供許多陣列相關的工具函式,多加瞭解可以幫助我們快速寫出易讀的程式碼。

2.5.3.5 字串型別

字串對應的類別是 String,字串值是不變的 (immutable),一經指派就不會再改變。字串是由一系列字元所組成,所以可以把它當成是字元陣列,像存取陣列一樣取得某一字元,例如:

val s = "Hello, world!"
println(s[1]) // [結果] e

我們可以像 Java 一樣使用 + 來串接其他資料型別的值,但前提是字串要在 + 前面,否則編譯時會發生錯誤,例如:

val a = "abc" + 1
// val a = 1 + "abc" // 錯誤

這是因為數字型別中的 plus() 方法並沒有支援字串的參數,但是字串的 plus() 方法則接受任何類型的參數。+ 號是 plus() 方法的重載。

2.5.4 Unit 型別

Unit 型別只有一個唯一值,就是 Unit 物件,它對應到 Java 中的 void 空型別。一個函式如果沒有回傳值,它預設就是回傳 Unit 型別。可以省略不寫。

2.5.5 Any 型別

Any 型別是 Kotlin 中所有類別的根,每個 Kotlin 類別都繼承自 Any,它相當於 Java 中的 Object 類別。

2.6 型別檢查

在執行時期,我們可以檢查某物件是不是我們所想要的資料型別,避免發生錯誤。is 可以用來檢查該物件是否屬於某個型別,或使用 !is 反過來檢查該物件不是屬於某個型別。請看範例:

val a: Any = "123"
println(a is Int) // [結果] false
println(a !is Int) // [結果] true

val b: Any = 123
println(b is Int) // [結果] true

2.7 智慧轉型

前面有提過,Kotlin 沒有隱含 (implicit) 轉型的機制,「隱含」帶有不確定性,對於程式的穩定性沒有好處。

Kotlin 提供智慧轉型 (Smart Casts) 機制,當我們自行檢查資料型別之後,如果符合該型別,Kotlin 會自動幫我們做轉型,由於有事先檢查,因此不會有模糊空間,省去了手動轉型這個動作,讓我們看看以下的例子:

val a: Any = "abc"
if (a is String) {
  // 如果進入這個區塊,表示變數 a 是字串
  // a 智慧轉型成 String,我們就能存取它的屬性
  print(a.length)
}

當我們用 is 檢查變數的結果是一個字串時,就可以把該變數當成字串來用,因為 Kotlin 可以毫無疑問的把它轉成字串型別。

以下這個範例也是可以的,因為 if 運算是從左到右,如果左邊的運算結果成立,才會繼續右邊的運算,也就是說,只要左邊的條件不成立,就不會繼續右邊的條件判斷。以此例來說,當 && (AND 運算) 的左邊 a is String 條件成立,才會執行 a.length > 0 第二個條件:

if (a is String && a.length > 0) {
  print(a[0])
}

另外,我們也可以在檢查型別不符合時執行 return ,這樣之後的敍述都不會被執行,也就是說,如果通過檢查,那在之後的敍述中,該變數已經被自動轉型了,請看以下範例:

fun demo() {
  val a: Any = 123
  if (a !is String) return
  
  // 以下這行,只有在 a 是 String 的情況下,才會被智慧轉型並執行
  println(a.length)
}

2.8 資料型別的轉換

我們可以明確的將某型別轉換成另一個型別,使用 asas? 關鍵字,兩者的差別在於一個是不安全的 (Unsafe),一個是安全的 (Safe)。

所謂的不安全,是指當轉型不成功時,會收到 ClassCastException 的例外錯誤;而安全的轉型則不會出現例外錯誤,而是回傳 null,所以安全的轉型是 nullable 的。

2.8.1 安全的轉型

使用 as? 來做安全的轉型,它的行為像是「可以幫我轉成某個資料型別嗎?如果無法轉成該型別,請給我 null」這樣,我們只要檢查轉型結果是否為 null 即可,請看範例:

val a: Any = 123
println(a as? String)
// [結果] null
// a 沒有被轉型
println(a is String)
// [結果] false

val b: Any = 123
println(b as? Int)
// [結果] 123
// b 已被轉型成 Int
println(b is Int)
// [結果] true

2.8.2 不安全的轉型

使用 as 來轉型,它的行為像是「無論如何幫我轉成某個資料型別」,這種強迫轉型的結果就是順利的話就沒事,不順利的話就會發生 ClassCastException 的例外,請看範例:

val a: Any = 123
println(a as String) // 發生 ClassCastException

請儘量避免寫出這種程式碼,或是透過補捉例外來做防範,請看範例:

val a: Any = 123
try {
  println(a as String)
} catch (e: ClassCastException) {
  println("錯誤!無法轉型為字串")
}


繼續閱讀:Kotlin 實戰範例 (3) 基礎 (Null、相等、字串、註解)

完整內容可以參考電子書:Google Play Pubu 樂天 Kobo

本文網址:https://blog.tonycube.com/2020/07/kotlin-by-example-2-basic-variable-types.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

我要留言

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。