從Java到Kotlin(八)

陳子豪發表於2019-03-04

Kotlin的其他技術

目錄

一、解構宣告
二、區間
三、型別檢查與轉換
四、this表示式
五、相等性
六、操作符過載
七、空安全
八、異常
九、型別別名


一、解構宣告

解構宣告能同時建立多個變數,將物件中的資料解析成相對的變數。舉個例子:

//建立一個資料類User
data class User(var name: String, var age: Int)

//獲得User的例項
var user = User("Czh", 22)
//宣告變數 name 和 age
var (name, age) = user

println("name:$name  age:$age")
//輸出結果為:name:Czh  age:22
複製程式碼

上面程式碼中用解構宣告同時建立兩個變數的時候,會被編譯成以下程式碼:

//指定變數name的值為user第一個引數的值
var name = user.component1()
//指定變數name的值為user第二個引數的值
var age = user.component2()

println("name:$name  age:$age") 
//輸出結果為:name:Czh  age:22
複製程式碼
  • 解構宣告和Map
    Map可以儲存一組key-value鍵值對,通過解構宣告可以把這些值解構出來。如下所示:
var map = mutableMapOf<String, Any>()
map.put("name", "Czh")
map.put("age", 22)
for ((key, value) in map) {
    println("$key$value")
}
複製程式碼

執行程式碼,輸出結果:

從Java到Kotlin(八)

二、區間

1.in

假如現在要判斷 i 是否在 1-5 內,可以這樣寫:

if (i in 1..5) {
    println("i 在 1-5 內")
}
複製程式碼

上面程式碼中,1..5指的是 1-5,in指的是在...範圍內,如果 i 在範圍 1-5 之內,將會執行後面的程式碼塊,輸出結果。如果想判斷 i 是否不在 1-5 內,可以這樣寫:

//!in表示不在...範圍內
if (i !in 1..5) {
    println("i 不在 1-5 內")
}
複製程式碼

上面兩段程式碼等同於:

if (i >= 1 && i <= 5) {
    println("i 在 1-5 內")
}
if (i <= 1 && i >= 5) {
    println("i 不在 1-5 內")
}
複製程式碼

2.downTo

如果想輸出 1-5 ,可以這樣寫:

for (i in 1..5) println(i)
//輸出12345
複製程式碼

如果倒著來:

for (i in 5..1) println(i)
//什麼也不輸出
複製程式碼

這個時候可以用downTo函式倒序輸出 5-1

for (i in 5 downTo 1) println(i)
複製程式碼

3.step

上面的程式碼順序輸出12345或倒序54321,按順序+1或者-1,也就是步長為1。如果要修改步長,可以用step函式,如下所示:

 for (i in 1..5 step 2) println(i) 
//輸出135

//倒序
for (i in 1 downTo 5 step 2) println(i) 
//輸出531
複製程式碼

4.until

上面的程式碼中,使用的範圍都是閉區間,例如1..5的區間是[1,5],如果要建立一個不包括其結束元素的區間,即區間是[1,5),可以使用until函式,如下所示:

for (i in 1 until 5) println(i)
//輸出1234
複製程式碼

三、型別檢查與轉換

1.is操作符

在Kotlin中,可以通過is操作符判斷一個物件與指定的型別是否一致,還可以使用is操作符的否定形式!is,舉個例子:

var a: Any = "a"
if (a is String) {
    println("a是String型別")
}
if (a !is Int) {
    println("a不是Int型別")
}
複製程式碼

執行程式碼,輸出結果為:

從Java到Kotlin(八)

2.智慧轉換

在Kotlin中不必使用顯式型別轉換操作,因為編譯器會跟蹤不可變值的is檢查以及顯式轉換,並在需要時自動插入(安全的)轉換。舉個例子:

var a: Any = "a"
if (a is String) {
    println("a是String型別")
    println(a.length) // a 自動轉換為String型別
    //輸出結果為:1
}
複製程式碼

還可以反向檢查,如下所示:

if (a !is String) return
print(a.length) // a 自動轉換為String型別
複製程式碼

在 && 和 || 的右側也可以智慧轉換:

// `&&` 右側的 a 自動轉換為String
if (a is String && a.length > 0)

// `||` 右側的 a 自動轉換為String
if (a is String || a.length > 0)
複製程式碼

在when表示式和while迴圈裡也能智慧轉換:

when(a){
    is String -> a.length
    is Int -> a + 1
}
複製程式碼

需要注意的是,當編譯器不能保證變數在檢查和使用之間不可改變時,智慧轉換不能用。智慧轉換能否適用根據以下規則:

  • val 區域性變數——總是可以,區域性委託屬性除外;
  • val 屬性——如果屬性是 private 或 internal,或者該檢查在宣告屬性的同一模組中執行。智慧轉換不適用於 open 的屬性或者具有自定義 getter 的屬性;
  • var 區域性變數——如果變數在檢查和使用之間沒有修改、沒有在會修改它的 lambda 中捕獲、並且不是區域性委託屬性;
  • var 屬性——決不可能(因為該變數可以隨時被其他程式碼修改)

3.強制型別轉換

在Kotlin中,用操作符as進行強制型別轉換,如下所示:

var any: Any = "abc"
var str: String = any as String
複製程式碼

但強制型別轉換是不安全的,如果型別不相容,會丟擲一個異常,如下所示:

var int: Int = 123
var str: String = int as String
//丟擲ClassCastException
複製程式碼

4.可空轉換操作符

null不能轉換為 String,因該型別不是可空的。舉個例子:

var str = null
var str2 = str as String
//丟擲TypeCastException
複製程式碼

解決這個問題可以使用可空轉換操作符as?,如下所示:

var str = null
var str2 = str as? String
println(str2) //輸出結果為:null
複製程式碼

使用安全轉換操作符as?可以在轉換失敗時返回null,避免了丟擲異常。

四、this表示式

為了表示當前的接收者我們使用this表示式。當this在類的成員中,this指的是該類的當前物件;當this在擴充套件函式或者帶接收者的函式字面值中,this表示在點左側傳遞的接收者引數。

  • 限定的this 如果this沒有限定符,它指的是最內層的包含它的作用域。如果要訪問來自外部作用域的this(一個類或者擴充套件函式, 或者帶標籤的帶接收者的函式字面值)我們使用this@label,其中 @label 是一個代指this來源的標籤。舉個例子:
class A { // 隱式標籤 @A
    inner class B { // 隱式標籤 @B
        fun Int.foo() { // 隱式標籤 @foo
            val a = this@A // A 的 this
            val b = this@B // B 的 this

            val c = this // foo() 的接收者,一個 Int
            val c1 = this@foo // foo() 的接收者,一個 Int

            val funLit = lambda@ fun String.() {
                val d = this // funLit 的接收者
            }


            val funLit2 = { s: String ->
                // foo() 的接收者,因為它包含的 lambda 表示式
                // 沒有任何接收者
                val d1 = this
            }
        }
    }
}
複製程式碼

五、相等性

在Kotlin中存在結構相等和引用相等兩中相等判斷。

1.結構相等

使用equals()==判斷,如下所示:

var a = "1"
var b = "1"
if (a.equals(b)) {
    println("a 和 b 結構相等")
    //輸出結果為:a 和 b 結構相等
}

var a = 1
var b = 1
if (a == b) {
    println("a 和 b 結構相等")
    //輸出結果為:a 和 b 結構相等
}
複製程式碼

2.引用相等

引用相等指兩個引用指向同一物件,用===判斷,如下所示:

data class User(var name: String, var age: Int)

var a = User("Czh", 22)
var b = User("Czh", 22)
var c = b
var d = a
if (c == d) {
    println("a 和 b 結構相等")
} else {
    println("a 和 b 結構不相等")
}
if (c === d) {
    println("a 和 b 引用相等")
} else {
    println("a 和 b 引用不相等")
}
複製程式碼

執行程式碼,輸出結果為:

從Java到Kotlin(八)

六、操作符過載

Kotlin允許對自己的型別提供預定義的一組操作符的實現,這些操作符具有固定的符號表示 (如 + 或 *)和固定的優先順序。為實現這樣的操作符,我們為相應的型別(即二元操作符左側的型別和一元操作符的引數型別)提供了一個固定名字的成員函式或擴充套件函式。 過載操作符的函式需要用 operator 修飾符標記。

過載操作符

+是一個一元操作符,下面來對一元操作符進行過載:

//用 operator 修飾符標記
operator fun String.unaryPlus(): String {
    return this + this
}

//呼叫
var a = "a"
println(+a)  //輸出結果為:aa
複製程式碼

當編譯器處理例如表示式 +a 時,它執行以下步驟:

  • 確定 a 的型別,令其為 T;
  • 為接收者 T 查詢一個帶有 operator 修飾符的無參函式 unaryPlus(),即成員函式或擴充套件函式;
  • 如果函式不存在或不明確,則導致編譯錯誤;
  • 如果函式存在且其返回型別為 R,那就表示式 +a 具有型別 R;

除對一元操作符進行過載外,還可以對其他操作符進行過載,其過載方式和原理大致相同。下面來一一列舉:

1.一元操作符

表示式 對應的函式
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a++ a.inc()
a-- a.dec()

2.二元操作符

表示式 對應的函式
a+b a.plus(b)
a-b a.minus(b)
a*b a.times(b)
a/b a.div(b)
a%b a.mod(b)
a..b a.rangeTo(b)

3.in操作符

表示式 對應的函式
a in b b.contains(a)
a !in b !b.contains(a)

4.索引訪問操作符

表示式 對應的函式
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)

5.呼叫操作符

表示式 對應的函式
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

6.廣義賦值

表示式 對應的函式
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b), a.modAssign(b)(已棄用)

7.相等與不等操作符

表示式 對應的函式
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

8.比較操作符

表示式 對應的函式
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

七、空安全

在Java中,NullPointerException 可能是最常見的異常之一,而Kotlin的型別系統旨在消除來自程式碼空引用的危險。

1.可空型別與非空型別

在Kotlin中,只有下列情況可能導致出現NullPointerException:

  • 顯式呼叫 throw NullPointerException();
  • 使用了下文描述的 !! 操作符;
  • 有些資料在初始化時不一致;
  • 外部 Java 程式碼引發的問題。

在 Kotlin 中,型別系統區分一個引用可以容納 null (可空引用)還是不能容納(非空引用)。 例如,String 型別的常規變數不能容納 null:

從Java到Kotlin(八)
如果要允許為空,我們可以宣告一個變數為可空字串,在字串型別後面加一個問號?,寫作 String?,如下所示:

var b: String? = "b"
b = null
複製程式碼

2.安全呼叫操作符

接著上面的程式碼,如果你呼叫a的方法或者訪問它的屬性,不會出現NullPointerException,但如果呼叫b的方法或者訪問它的屬性,編譯器會報告一個錯誤,如下所示:

從Java到Kotlin(八)
這個時候可以使用安全呼叫操作符,寫作?.,在b後面加安全呼叫操作符,表示如果b不為null則呼叫b.length,如下所示:

b?.length
複製程式碼

安全呼叫操作符還能鏈式呼叫,例如一個員工 Bob 可能會(或者不會)分配給一個部門, 並且可能有另外一個員工是該部門的負責人,那麼獲取 Bob 所在部門負責人(如果有的話)的名字,我們寫作:

Bob?.department?.head?.name
//如果Bob分配給一個部門
//執行Bob.department.head?獲取該部門的負責人
//如果該部門有一個負責人
//執行Bob.department.head.name獲取該負責人的名字
複製程式碼

如果該鏈式呼叫中任何一個屬性為null,整個表示式都會返回null。 如果要只對非空值執行某個操作,安全呼叫操作符可以與let一起使用,如下所示:

val listWithNulls: List<String?> = listOf("A", null, "B")
for (item in listWithNulls) {
    item?.let { println(it) }
}
複製程式碼

執行程式碼,輸出結果為:

從Java到Kotlin(八)

  • 安全的型別轉換
    如果物件不是目標型別,那麼常規型別轉換可能會導致 ClassCastException。 另一個選擇是使用安全的型別轉換,如果嘗試轉換不成功則返回null,如下所示:
val i: Int? = i as? Int
複製程式碼
  • 可空型別的集合
    如果你有一個可空型別元素的集合,並且想要過濾非空元素,你可以使用filterNotNull來實現。如下所示:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
複製程式碼

3.Elvis 操作符

先看一段程式碼:

val i: Int = if (b != null) b.length else -1
val i = b?.length ?: -1
複製程式碼

這兩行程式碼表達的都是“如果b不等於null,i = b.length;如果b等於null,i = -1”。第一行程式碼用的是if表示式,而第二行程式碼使用了Elvis操作符,寫作?:Elvis操作符表示如果?:左側表示式非空,就使用左側表示式,否則使用右側表示式。 請注意,因為throwreturn在Kotlin中都是表示式,所以它們也可以用在Elvis操作符右側。如下所示:

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ……
}
複製程式碼

4. !! 操作符

!!操作符將任何值轉換為非空型別,若該值為空則丟擲異常。如下所示:

var a = null
a!!
//執行程式碼,丟擲KotlinNullPointerException
複製程式碼

八、異常

Kotlin中所有異常類都是Throwable類的子類。每個異常都有訊息、堆疊回溯資訊和可選的原因。 使用throw表示式可以丟擲異常。舉個例子:

throw NullPointerException("NPE")
複製程式碼

使用try表示式可以捕獲異常。一個try表示式可以有多個catch程式碼段;finally程式碼段可以省略。舉個例子:

try {
    //捕獲異常
} catch (e: NullPointerException) {
    //異常處理
} catch (e: ClassNotFoundException) {
    //異常處理
} finally {
    //可選的finally程式碼段
}
複製程式碼

因為Try是一個表示式,所以它可以有一個返回值。舉個例子:

val a: Int? = try {
    parseInt(input) 
} catch (e: NumberFormatException) {
    null 
}
複製程式碼

try表示式的返回值是 try塊中的最後一個表示式或者是catch塊中的最後一個表示式。finally塊中的內容不會影響表示式的結果。

九、型別別名

Kotlin提供型別別名來代替過長的型別名稱,這些型別別名不會引入新型別,且等效於相應的底層型別。可以通過使用關鍵字typealias修改型別別名,如下所示:

//使用關鍵字typealias修改型別別名Length
//相當於 Length 就是一個 (String) -> Int 型別
typealias Length = (String) -> Int

//呼叫
fun getLength(l: Length) = l("Czh")
//編譯器把 Length 擴充套件為 (String) -> Int 型別
val l: Length = { it.length }
println(getLength(l)) //輸出結果為:3
複製程式碼

使用型別別名能讓那些看起來很長的型別在使用起來變得簡潔,如下所示:

typealias MyType = (String, Int, Any, MutableList<String> ) -> Unit
//當我們使用的時候
var myType:MyType 
//而不需要寫他原來的型別
//var myType:(String, Int, Any, MutableList<String> ) -> Unit
複製程式碼

總結

相對於Java來說,Kotlin有很多新的技術和語法糖,這也是為什麼使用Kotlin來開發Android要優於Java。運用好這些新的東西,能大大加快開發速度。

參考文獻:
Kotlin語言中文站、《Kotlin程式開發入門精要》
推薦閱讀:
從Java到Kotlin(一)為什麼使用Kotlin
從Java到Kotlin(二)基本語法
從Java到Kotlin(三)類和介面
從Java到Kotlin(四)物件與泛型
從Java到Kotlin(五)函式與Lambda表示式
從Java到Kotlin(六)擴充套件與委託
從Java到Kotlin(七)反射和註解
從Java到Kotlin(八)Kotlin的其他技術
Kotlin學習資料總彙


更多精彩文章請掃描下方二維碼關注微信公眾號"AndroidCzh":這裡將長期為您分享原創文章、Android開發經驗等! QQ交流群: 705929135

從Java到Kotlin(八)

相關文章