3.1 安全的 Null
Null 的發明者 Tony Hoare 曾道歉說,這是他犯下的 十億美元的錯誤(The Billion Dollar Mistake),可見 null
有多危險。在 Java 中,只要存取 null reference 的成員就會引發 NullPointerException
(空指標例外,簡稱 NPE),造成程式崩潰,不得不小心。
Kotlin 致力於消除危險的 null
參考 (reference) 引用,在型別系統的設計上已經可以避免大多數 NPE 的發生,除非我們明確地丟出 NPE,例如:throw NullPointerException()
、或是使用 !!
符號、或是由外部 Java 程式碼所造成等等。
3.1.1 Nullable 型別及 Non-Null 型別
在 Kotlin 中,型別系統區分成兩種,一種是可以接受 Null (nullable references) 的,在型別名稱後面加上問號 ?
來表示;另一種是不能接受 Null (non-null references) 的。
先來看看不能接受 Null 的例子:
var name: String = "Tony"
name = null // <-- 編譯錯誤
我們宣告的變數 name
的型別 String
不允許 null
值,當我們指派給它的值為 null
時,就會編譯失敗。這種設計的好處在於,我們在宣告變數時就能確定它是有值的,即使之後修改其值,也絕對不可能是 null
。因為在編譯時就能發現這個問題,在程式中存取此變數時就不會有模棱兩可的情況。
接下來看看接受 Null (nullable references) 的例子:
var name: String? = "Tony"
name = null // <-- 編譯成功
當我們在型別名稱後面加上問號 ?
,就表示此變數接受 null
值,因此這個變數的值可能有兩種情況:值或 null
。當我們在使用這個變數時就必須額外檢查 null
,否則編譯器不會讓這個程式碼通過,這樣我們的程式碼將會變得更加穩固。
現在,我們來看看這兩種型別對我們的程式造成了什麼影響:
val a: String = "Tony"
println(a.length)
val b: String? = "Tony"
println(b.length) // <-- 編譯失敗
我們可以毫不猶豫的存取變數 a
的屬性,並且知道它不會出問題 (引發 NPE);同樣的動作對於變數 b
則有風險,立即被編譯器發現而編譯失敗,我們必須檢查該變數的值是否為 null
,編譯器才能讓我們繼續,接下來讓我們看看如何檢查 null
。
3.1.2 Null 的檢查
針對可能為 null
的變數,在存取之前要先用 if
檢查其值,確定不是 null
才能呼叫其成員,請看範例:
val b: String? = "Tony"
if (b != null) {
println(b.length)
}
由於 if
中的條件是從左向右執行,因此我們也可以同時檢查 null
並呼叫其成員 (類別中的屬性或方法),請務必依照這個順序指定條件,請看範例:
val b: String? = "Tony"
if (b != null && b.isNotEmpty()) {
println(b.toUpperCase())
}
3.1.3 安全呼叫成員
Kotlin 提供比 if
檢查更有效率的方式,使用 ?.
符號來安全呼叫成員,請看範例:
val b: String? = "Tony"
println(b?.length)
這樣是不是簡單、快速又方便。?.
的意思是「如果變數 b
有值就存取它的 length
屬性,如果是 null
就不要做任何動作」,這樣就不需要額外使用 if
來檢查了。程式碼不僅簡短,還能一眼就知道意圖。
有一點要注意的是,一旦我們使用了一個安全呼叫,在其後的呼叫鏈也必須使用安全呼叫,請看以下範例:
val b: String? = "Tony"
println(b?.toUpperCase()?.replace("Y", "")?.length)
這個安全呼叫鏈中,只要其中一個呼叫回傳 null
,就會中斷整個呼叫鏈,結果只會得到 null
而不會發生任何錯誤,如果沒有使用安全呼叫鏈,就必須使用 if
條件判斷,程式碼將變得冗長而不易閱讀。
3.1.4 貓王運算符號
使用「安全呼叫」其實有個小缺點,由於最後的結果值可能為 null
,所以最終我們還是得判斷結果值是不是 null
,似乎永遠離不開 if
條件判斷。Kotlin 提供一個方便的貓王 (Elvis) 運算符號 ?:
,它可以讓我們在得到 null
結果時指定預設值,這樣就能讓整個安全呼叫鏈一氣呵成,請看範例:
val b: String? = null
println(b?.toUpperCase()?.replace("Y", "")?.length ?: -1)
註:被稱為 貓王運算符號 的原因是這個符號和 貓王的顏文字 ?;J 很像,所以就被這樣命名了。貓王本人照片。
3.1.5 危險的強制呼叫
有時候我們非常有自信的確定變數是有值的,不打算使用安全呼叫,這時我們可以使用雙驚嘆號 !!
來強制呼叫,不過我們必須瞭解,如果該變數是 null
的話就會引發 NPE,請看範例:
val b: String? = "Tony"
println(b!!.length)
使用這種方式非常不安全,請儘量避免或記得補捉 NPE。
3.1.6 安全轉型及預設值
在使用 as
轉型時,如果轉型失敗會丟出 ClassCastException
,所以建議使用安全的轉型 as?
,而安全的轉型也可以搭配貓王運算符號來指定預設值,請看範例:
val a: Any = 123
println(a as? String ?: "無法轉型為字串")
這樣在轉型失敗時就會使用預設值。
3.2 相等比較
Kotlin 提供兩種類型的相等比較:
- 參考相等 (或稱位址相等):即兩個物件指向同一個參考 (位址);使用連續三個等號
===
來比較參考相等,!==
則比較參考不相等。 - 結構相等 (結構指存在該位址中的值):只判斷值是否相等,不管位址是否相等。使用
equals()
方法 來比較結構 (值),或是使用==
符號;!=
則判斷不相等。
重要!Kotlin 的 ==
和 Java 的 ==
行為是不同的,Java 的 ==
比較的是參考 (位址), equals()
方法則是比較值。Kotlin 使用 ==
或 equals()
來比較值,使用 ===
比較參考 (位址)。
3.2.1 結構的比較
當我們使用 ==
符號時,實際上會轉換成如下的程式碼:
a?.equals(b) ?: (b === null)
如果 a
不是 null
,會呼叫 equals()
方法來比較,如果 a
是 null
則比較 b
是否為 null
。所以 ==
符號實際上做的事比 equals()
方法還多,因此除非必要,否則大多數時候都是使用 ==
符號來做結構的比較。
另外,要比較某變數是否為 null
時,必須使用 ===
比較其參考,但是我們不必明確的使用 ===
,當我們使用 ==
來比較是否為 null
時,Kotlin 會自動幫我們轉換為 ===
。
對於不同的數值型別,必須轉換成相同型別才能比較,或是使用 compareTo()
方法,請看範例:
val a: Int = 1
val b: Long = 1L
val c: Double = 1.0
println(a.toLong() == b) // true
println(a.toDouble() == c) // true
println(a.compareTo(b)) // 0
println(a.compareTo(c)) // 0
compareTo()
的結果,0
表示相等,-1
表示小於,1
表示大於。
針對數值這種在 Java 中被視為原始型別 (primitive type) 的值使用 ===
符號是沒有必要的,它的結果等同於 ==
。
3.2.2 物件的比較
當我們建立一個物件時,會得到一個參考 (記憶體位址),要比較兩個物件是否屬於同一個參考,必須使用 ===
,請看範例:
data class User(val name: String)
fun main() {
val a = User("Tony")
val b = User("Tony")
val c = a // 同一個物件,即相同參考
println(a === b) // false
println(a === c) // true
// 比較結構
println(a == b) // true
}
這裡要注意的是,因為我們使用 Kotlin 提供的 data class
來建立類別,比較結構時才會相等,這是因為 data class
幫我們實做了等式判斷用的 equals()
方法,如果使用一般的類別而又沒有實作 equals()
方法,比較結構的結果等同於比較參考的結果。
在 Java 中想要單純比較物件的結構比較麻煩,必須自行實作 equals()
方法才行,簡單示範如下:
/* Java */
public class Jpp {
public static void main(String[] args) {
User a = new User("Tony");
User b = new User("Tony");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
}
}
class User {
private String name;
User(String name) {
this.name = name;
}
// 如果沒有實作以下方法,a.equals(b) 的結果將為 false
@Override
public boolean equals(Object obj) {
try {
return this.name.equals(((User)obj).name);
} catch (Exception e) {
return false;
}
}
}
在 Java 中,如果沒有實作自己版本的 equals()
方法,預設會使用從 Object
繼承而來的比對方式,這個比對方式只是簡單的用 ==
來比對兩個物件的參考,我們看到範例中,使用 ==
比對的結果就是 false
,也就是說,在 Java 中比較兩個物件時,使用 equals()
方法或 ==
的結果都一樣,只能自行實作比對邏輯。
實際上,我們還少說了 hashCode
的部分,根據規則,只要有實作 equals()
就必須同時實作 hashCode
。(這部分要解釋的東西太多,可以參考電子書的內容)
3.3 字串
3.3.1 字串模板
我們經常將變數和一段文字串接來表達完整的內容,在 Java 裡只能用 +
來串接,即使使用 String.format()
也不是那麼清楚、簡單,來看看 Java 的例子:
/* Java */
String name = "Tony";
int age = 5;
// 使用 +
System.out.println("My name is " + name + ", " + age + " years old.");
// 使用 String.format()
System.out.println(String.format("My name is %s, %d years old.", name, age));
我們得查一下文件才知道 %s
對應到字串、%d
對應到整數。
來看看 Kotlin 的字串模板怎麼用:
val name = "Tony"
val age = 5
println("My name is $name, $age years old.")
在字串中要取用任何變數的值,只要直接使用該變數的名稱,並在前面加上 $
即可,這樣是不是方便多了,整個字串也非常容易閱讀。而且不止這樣,如果我們想要取用該變數的屬性或做任何的運算,只要額外使用大括號 {}
把它包圍即可,請看範例:
println("My name have ${name.length} characters.")
// [結果] My name have 4 characters.
println("1 + 2 = ${1 + 2}")
// [結果] 1 + 2 = 3
3.3.2 原生字串
有時候我們需要輸出一段經過排版的文字,包括縮排、斷行及空白等等內容,可以用一對三個雙引號 """
來包圍該內容,在此區塊中的內容會如實呈現,請看範例:
fun main() {
val code = """
val names = arrayOf("Tony", "Tom", "John")
for (name in names) {
println(name)
}
"""
println(code)
}
/* [結果]
val names = arrayOf("Tony", "Tom", "John")
for (name in names) {
println(name)
}
*/
我們可以注意到輸出結果的上面多了一行空白,前面也有縮排,如果我們想在輸出結果中讓內容不要有多餘的縮排及空行,我們可以用 trimMargin()
函式來消除,請看範例:
fun main() {
val code = """
|val names = arrayOf("Tony", "Tom", "John")
|for (name in names) {
| println(name)
|}
""".trimMargin()
println(code)
}
trimMargin()
可以讓我們指定行的開頭,預設使用 |
符號,輸出的內容會以這個符號當成行的起點。我們也可以換成使用其他符號,例如這樣 trimMargin(">")
;如果我們只是想要忽略縮排,可以直接使用 trimIndent()
即可,它不需要使用起頭符號,但是結果和上例一樣。我們一樣可以在原生字串中使用字串模板的符號來取用變數值。
註:Java 13 也會有相似的功能稱為 Text Blocks (Preview) 預覽版。
3.4 註解
註解可以用來說明程式碼的意圖,程式編譯時會忽略註解。
3.4.1 單行註解
以雙斜線 //
表示:
// 這行之後的都是註解
3.4.2 區塊註解
以一對 /*
及 */
包圍的區塊,註解的內容較多時使用:
/* 這個區塊
都是註解 */
Java 的區塊註解不能是巢狀的,Kotlin 的可以:
/* 1111
/* 2222 */
3333
*/
3.4.3 文件式註解
文件式註解可以讓我們將註解輸出為說明文件。Kotlin 使用的程式碼文件稱為 KDoc
,如同 JavaDoc
,語法相同但多了自己特有的標籤,並且可以在行內標記 (inline markup) 中使用 Markdown 語法。文件式註解以一對 /**
及 */
包圍的區塊表示:
/**
哈囉類別
@author Tony
*/
class Hello {
// ...
}
如果要產生說明文件,可以用 Dokka 文件產生工具來輸出程式碼文件。在 build.gradle
檔中加入 Dokka
:
plugins {
id 'org.jetbrains.dokka' version '0.10.0'
}
repositories {
jcenter()
// 或 maven { url 'https://dl.bintray.com/kotlin/dokka' }
}
dokka {
outputFormat = 'html'
outputDirectory = "$buildDir/kdoc"
}
然後在命令列執行:
./gradlew dokka
就會在 build/kdoc
目錄下產生 html 格式的文件。
繼續閱讀:Kotlin 實戰範例 (4) 基礎 (條件控制、循環執行)
完整內容可以參考電子書:Google Play Pubu 樂天 Kobo
由 Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀
我要留言
留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。