Kotlin筆記

一、Kotlin基礎(chǔ)

1.1 變量

在Kotlin中變量分為可變引用var和不可變引用val,val對應(yīng)的是java中的final變量。盡管val的引用地址是不可變的,但它指向的對象完全是可變的。在val變量的代碼塊執(zhí)行期間,它只能進(jìn)行唯一一次初始化,如果編譯器能確保只有唯一一條初始化語句會被執(zhí)行,可以根據(jù)條件使用不同的值來初始化。

// 如果編譯器可以推導(dǎo)出類型,那么不用顯示聲明類型。
val answer = 10
// 確保只有唯一一條初始化語句被執(zhí)行
val answer: Int
if (...) {
  answer = 10
} else {
  answer = 20
}

1.2 字符串模板

字符串模板指在String值中引用局部變量,只需在變量名前加上$,其效率等價于Java中的字符串拼接。除了直接引用變量,還可以使用表達(dá)式,只需用{}包裹表達(dá)式即可。

// 引用變量
val s = "world"
print("hello $s")
// 引用表達(dá)式
val array = mutableListOf("hello", "world")
println("message is ${array[0]} and ${array[1]}")

其實(shí)編譯后的代碼創(chuàng)建了一個StringBuilder對象,并把常量部分和變量部分附加上去。

1.3 類和屬性

在Kotlin中聲明一個實(shí)體類非常簡單,不需要像Java一樣聲明所有屬性和getter/setter方法。下方就是一個包含name屬性和isMarried屬性的Person實(shí)體類,它沒有聲明類的訪問權(quán)限,Kotlin中的默認(rèn)可見性就是public。

class Person(val name: String, var isMarried: Boolean)

Kotlin會為Person類的屬性生成getter/setter方法,name屬性是val變量,只會生成getter方法;而isMarried屬性是var變量,會生成getter和setter方法。

訪問Person對象的屬性是通過person.name的方式,內(nèi)部實(shí)際調(diào)用person.getName()方法;修改屬性是通過person.isMarried = true,實(shí)際調(diào)用person.setMarried(true)方法。對于那些在Java中定義的類,一樣可以使用Kotlin的屬性語法。

1.4 迭代語法

Kotlin在迭代數(shù)字時使用了區(qū)間的概念,有兩種常見的操作符如下所示,".."操作符表示閉區(qū)間,"until"操作符表示左閉右開區(qū)間。

// 表示[1, 10]
for (i in 1..10) {
    println(i)
}
// 表示[1, 10)
for (i in 1 until 10) {
    println(i)
}

這兩種迭代的步長都為1,如果想跳過一些數(shù)字,可以在迭代時指定步長。

for (i in 100 downTo 1 step 2) {
    // ......
}

迭代map也可以用很簡潔的模式,如下所示,使用for循環(huán)展開迭代中的集合的元素,把展開的結(jié)果存儲到了2個獨(dú)立的變量中。

val map = TreeMap<String, String>()
// 添加元素至map......
for ((key, value) in map) {
    // ......
}

1.5 異常處理

Kotlin中throw結(jié)構(gòu)是一個表達(dá)式,能作為另一個表達(dá)式的一部分使用。如果number值正常,則percentage變量會被初始化,否則變量不會被初始化,直接拋出異常。

val percentage = 
    if (number in 0..100)
        number
    else 
        throw IllegalArgumentException("......")

與之類似的是,異常處理的"try"也可以作為表達(dá)式,下方的代碼在不出現(xiàn)異常時可以得到正確地值,出現(xiàn)異常時number會被賦值為null。

val number = try {
    Integer.parseInt(str)
} catch (e: NumberFormatException) {
    null
}

二、函數(shù)

2.1 Kotlin函數(shù)基礎(chǔ)

在Kotlin中可以為函數(shù)的參數(shù)指定默認(rèn)值,調(diào)用時可以省略這些有默認(rèn)值的參數(shù)。調(diào)用Kotlin函數(shù)可以像Java一樣按照參數(shù)的順序傳參,在參數(shù)較多時也可以顯示指定參數(shù)的名字來避免混淆。

// 函數(shù)定義
fun collect(collection: Collection<String>, separator: String = ", ",
            prefix: String = "<", postfix: String = ">") {
    // ......
}

// 函數(shù)調(diào)用,顯示指定了參數(shù)名,并省略了兩個默認(rèn)參數(shù)
collect(collection = strs, separator = " ")

由于Java沒有參數(shù)默認(rèn)值的概念,當(dāng)從Java中調(diào)用Kotlin函數(shù)時必須顯示指定所有參數(shù)值。如果需要從Java代碼中頻繁地調(diào)用,并且希望能對Java的調(diào)用者更簡便,可以使用@JvmOverloads注解,它會讓編譯器為函數(shù)生成Java重載函數(shù),調(diào)用時就可以從最后一個參數(shù)開始省略。

2.2 擴(kuò)展函數(shù)與屬性

Kotlin可以為現(xiàn)有的類添加擴(kuò)展函數(shù),用以平滑地與現(xiàn)有代碼集成。例如可以為String類添加一個擴(kuò)展函數(shù)來獲取字符串的最后一個字符。

package com.test

fun String.lastChar(): Char {
    return this[this.length - 1]
}

定義后的擴(kuò)展函數(shù)不會在整個項(xiàng)目范圍內(nèi)生效,在使用擴(kuò)展函數(shù)時需要導(dǎo)入。除了直接導(dǎo)入原函數(shù)外,還可以為擴(kuò)展函數(shù)指定別名。

// 導(dǎo)入擴(kuò)展函數(shù)
import com.test.lastChar
println("Kotlin".lastChar())

// 使用別名導(dǎo)入擴(kuò)展函數(shù)
import com.test.lastChar as last
println("Kotlin".last())

擴(kuò)展函數(shù)的本質(zhì)是靜態(tài)函數(shù),因此不存在重寫。當(dāng)從Java調(diào)用Kotlin的擴(kuò)展函數(shù)時,只需要通過文件名調(diào)用該擴(kuò)展函數(shù),并將接受者對象作為第一個參數(shù)傳入即可。

擴(kuò)展屬性實(shí)際是通過擴(kuò)展類的API來訪問屬性,例如可以為StringBuilder類定義一個lastChar屬性表示最后一個字符,內(nèi)部定義getter和setter方法,在Kotlin中訪問屬性時實(shí)際調(diào)用了getter或setter方法。當(dāng)從Java中訪問擴(kuò)展屬性時,需要顯式調(diào)用其getter或setter函數(shù)。

var StringBuilder.lastChar: Char {
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
}

2.3 Kotlin函數(shù)的其余特性

2.3.1 可變參數(shù)

在Kotlin中可以通過val list = listOf(1, 2, 3)這樣的形式來創(chuàng)建列表等集合,此時可以傳遞任意個參數(shù),這就是可變參數(shù)。在Java中可以通過...表示可變個參數(shù),而Kotlin中使用vararg,如下所示。

fun listOf<T>(vararg values: T): List<T> {
    //......
}

2.3.2 中綴調(diào)用

當(dāng)使用mapOf(1 to "one", 2 to "two", 3 to "three")來創(chuàng)建map時,可以通過to表示鍵值的對應(yīng)關(guān)系,to并不是關(guān)鍵字,而是表示一種函數(shù)調(diào)用。在聲明中綴調(diào)用時,需要使用infix修飾符,一個to函數(shù)的聲明如下。

infix fun Any.to(other: Any) = Pair(this, other)

三、類,對象和接口

3.1 類繼承結(jié)構(gòu)

3.1.1 接口

Kotlin聲明接口的關(guān)鍵字與Java一樣都是interface,并且可以像Java8一樣為接口的方法提供默認(rèn)實(shí)現(xiàn)。

interface clickable {
    fun click()
    fun showoff() = println("call showoff in clickable")
}

interface Focusable {
    fun setFocusable()
    fun showoff() = println("call showoff in focusable")
}

如果一個類實(shí)現(xiàn)了上面2個接口,那么必須實(shí)現(xiàn)showoff()方法,否則編譯器會報錯,如果想調(diào)用某一個接口的默認(rèn)實(shí)現(xiàn),則需通過super指定該接口/父類。

override fun showoff() = super<Clickable>.showoff()

Java中的類默認(rèn)是可以被繼承的,也可以重寫父類的方法,但是這也可能導(dǎo)致子類不正確行為。因此在Kotlin中,類和方法默認(rèn)都是final的,對可被繼承的類需要使用open修飾符,對每一個可被重寫的方法都要添加open修飾符。

open class Button: Clickable() {
    fun disable()
    open fun animate() {...}
    override fun click() {...}
}

3.1.2 可見性修飾符

Kotlin的可見性修飾符與Java一樣是public、protected與private,但是默認(rèn)的可見性有所不同:Java中為protected,Kotlin中為public。需要注意的是,Kotlin中的protected成員只在類和子類中可見,并且類的擴(kuò)展函數(shù)不能訪問它的private和protected成員。

Kotlin還有個關(guān)鍵字internal表示“只在模塊內(nèi)部可見”,模塊指的是一組一起編譯的Kotlin文件,internal對模塊提供了細(xì)節(jié)實(shí)現(xiàn)的封裝。在Java中這種封裝很容易被破壞,因?yàn)橥獠看a可以將類定義到相同的包中從而得到訪問模塊私有聲明的權(quán)限。

3.1.3 內(nèi)部類

Java中的內(nèi)部類隱式持有外部類的引用,內(nèi)部類可以直接訪問外部類的屬性和方法,如果不希望內(nèi)部類持有外部類的引用,可以使用static修飾內(nèi)部類。

Kotlin中的默認(rèn)內(nèi)部類與Java中static修飾的內(nèi)部類相同。如果希望內(nèi)部類持有外部類的引用,需要使用inner修飾內(nèi)部類。在內(nèi)部類中訪問外部類的引用時,需要使用this@Outer訪問外部類。

class Outer {
    inner class Inner {
        fun getOuterRef(): Outer = this@Outer
    }
}

3.2 構(gòu)造方法

Kotlin可以用很簡單的方式聲明一個類和它的構(gòu)造函數(shù),如下所示。User類包含nickname屬性,并且有一個以nickname為參數(shù)的構(gòu)造函數(shù)。

class User(val nickname: String)

這是聲明User類的簡寫,如果不采用簡寫,那么是這樣的。

class User constructor(_nickname: String) {
    val nickname: String
    init {
        nickname = _nickname
    }
}

如果需要將構(gòu)造函數(shù)私有化,可以使用private修飾構(gòu)造器。

class Secretive private constructor() {}

在Java中使用private修飾構(gòu)造器表示一個更通用的意思:這個類是一個靜態(tài)工具成員或是單例。Kotlin針對這種特性提供了語言級別的功能,例如我們之后會提到的lazy。

當(dāng)為一個類(例如Android中的視圖)聲明多個構(gòu)造函數(shù)時,可以通過super或this關(guān)鍵字使該構(gòu)造方法調(diào)用父類或者自己的構(gòu)造方法,如下所示。

class CustomView: View {
    // 調(diào)用當(dāng)前類的構(gòu)造方法
    constructor(context: Context): this(context, null) {
        // ......
    }

    // 調(diào)用父類的構(gòu)造方法
    constructor(context: Context, attr: AttributeSet): this(context, attr) {
        // ......
    }
}

3.3 實(shí)體類

在使用Java實(shí)現(xiàn)實(shí)體類時,我們一般需要重寫toString(), equals()hashCode()方法,現(xiàn)在來看看用Kotlin如何實(shí)現(xiàn),如下所示。實(shí)現(xiàn)equals()方法后,在Kotlin中可以直接通過"=="表示兩個對象是否equals,如果想要像Java中表示兩個對象的引用是否相等,那么需要使用"==="操作符。

class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) {
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }

    override fun toString(): String = "Client(name = $name, postalCode = $postalCode)"
}

順便提一下為什么要重寫hashCode()方法:如果只重寫equals()而不重寫hashCode()方法,那么對象的hash值就是對象引用地址的hash。在對HashMap等數(shù)據(jù)結(jié)構(gòu)調(diào)用add()方法時,會計算當(dāng)前對象的hashcode判斷集合中是否已經(jīng)存在同樣hash的對象,如果存在相等的hashcode,會再判斷是否存在equal的對象。顯而易見,如果不重寫hashcode()方法,向HashMap中不斷add()相等的對象時,由于它們的hashcode不相等,都會被添加到集合中。

每次實(shí)現(xiàn)一個實(shí)體類都要重寫這3個方法,看起來有點(diǎn)麻煩。不過Kotlin提供了data關(guān)鍵字來修飾實(shí)體類,toString(), equals()hashCode()這3個方法會被自動創(chuàng)建。

data class Client(val name: String, val postalCode: Int)

這種實(shí)體類的屬性都應(yīng)該修飾為val,表示對象創(chuàng)建后不可變,只有不可變對象才能作為HashMap的Key。而且在多線程編程中不可變對象也更有優(yōu)勢,因?yàn)椴挥脫?dān)心其他線程修改了它的狀態(tài)。

3.4 object關(guān)鍵字

通過object關(guān)鍵字可以很輕易地實(shí)現(xiàn)單例模式,object表示定義一個類并創(chuàng)建一個對象,如下所示。由于這是單例,可以直接通過Singleton.function1()這樣的形式調(diào)用單例的方法。在Java中則需使用Singleton.INSTANCE.function1()調(diào)用。

object Singleton {
    fun function1() {...}
}

當(dāng)object修飾的類是嵌套類時,在整個系統(tǒng)中同樣只具有一個實(shí)例。

data class Person(val name: String) {
    object NameComparator: Comparator<Person> {...}
}

使用companion object關(guān)鍵字可以構(gòu)建伴生對象,伴生對象定義在類中,表示這是當(dāng)前類的單例,可以直接通過A.function1()的方式調(diào)用伴生對象的方法。在Java中可以通過A.Companion.function1()的方式調(diào)用伴生對象的方法。

class A {
    companion object {
        fun function1() {...}
    }
    // 也可以為伴生對象指定名字, 調(diào)用時通過 A.Obj.function2()
    companion object Obj {
        fun function2() {...}
    }
}

四、Lambda編程

4.1 Lambda基礎(chǔ)

Lambda編程可以將函數(shù)作為值使用,在調(diào)用方法時直接傳遞一段代碼作為形參。例如可以定義一個函數(shù),表示求2個值的和。

val sumFun = {x: Int, y: Int -> x + y}

當(dāng)需要獲得集合中滿足某個條件的對象時,例如獲取Person集合中年級最大的對象,可以使用people.maxByOrNull({ p: Person -> p.age })。當(dāng)Lambda是函數(shù)調(diào)用的最后一個實(shí)參時,可以省略括號,而且可以用it指代Lambda的參數(shù),因此最后可以簡寫為people.maxByOrNull({ it.age })

那么這個maxByOrNull()做了什么呢?來看一下它的源碼。其中T泛型表示集合的類型,R泛型表示selector返回的結(jié)果。再看函數(shù)邏輯,maxByOrNull()函數(shù)遍歷了集合并對每個元素調(diào)用selector(e)方法得到v,最后得到集合中v最大的元素。

public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var maxElem = iterator.next()
    if (!iterator.hasNext()) return maxElem
    var maxValue = selector(maxElem)
    do {
        val e = iterator.next()
        val v = selector(e)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    } while (iterator.hasNext())
    return maxElem
}

在Java中使用匿名內(nèi)部類時,匿名內(nèi)部類中的代碼可以訪問外部的final對象,在Kotlin中可以做到同樣的事情。有意思的時,Kotlin允許在lambda內(nèi)部訪問非final變量甚至修改它們。當(dāng)從lambda內(nèi)部訪問外部變量時,稱這些變量被lambda捕捉,就像下方例子的prefix。

fun printMsg(messages: Collection<String>, prefix: String) {
    messages.forEach { 
        print("$prefix $it")
    }
}

當(dāng)捕捉final變量時,變量和lambda會被存儲并稍后執(zhí)行;當(dāng)捕捉非final變量時,該變量會被封裝到一個特殊的包裝器,隨后包裝器和lambda會被存儲并稍后執(zhí)行。

4.2 集合的函數(shù)式API

假設(shè)有個整型集合val list = listOf(1, 2, 3, 4),現(xiàn)在通過list來看集合API的功能。

  1. filter: 對集合過濾,傳入的lambda表示過濾條件。list.filter{ it % 2 == 0 }結(jié)果為{2, 4}
  2. map: 對集合中的每一個元素運(yùn)行l(wèi)ambda,得到一個全新的集合list.map{it * it}結(jié)果為 {1, 4, 9, 16}
  3. all: 判斷集合中的元素是否都滿足lambda的條件。list.all { it <= 4 }結(jié)果為 true
  4. any: 判斷集合中是否存在一個匹配lambda的元素list.any { it == 4 }結(jié)果為 true
  5. count: 判斷集合中有多少個元素滿足條件。list.count { it <= 3 }結(jié)果為3
  6. find: 找到一個滿足條件的元素,同義方法firstOrNull。list.find { it <= 3 }結(jié)果為1

除了上述這些基礎(chǔ)的集合API,還有一些可以轉(zhuǎn)換集合的API,例如groupBy、flatMap等。
groupBy根據(jù)集合元素的特征將它們劃分為不同的組,得到一個map,舉個栗子。

val people = listOf(Person("A", 20), Person("B", 19), Person("C", 20))
people.groupBy{ it.age }

這里根據(jù)Person的age分組,得到一個的map,其中key為20的有Person("A", 20), Person("C", 20)這2個元素,key為19的有Person("B", 19)這1個元素。

flatMap會根據(jù)lambda對元素做變換,然后把列表合并(平鋪)成一個列表。下方的代碼先把字符串轉(zhuǎn)為list,生成了[a, b, c]和[d, e, f]這2個list,隨后合并為一個,結(jié)果為[a, b, c, d, e, f]。

val strings = listOf("abc", "def")
strings.flatMap{ it.toList() }

4.3 序列:惰性集合操作

上面提到的map和filter等操作符會返回一個集合對象。假設(shè)當(dāng)前有一個需求,要得到people中名字以"A"開頭的名字列表,你可能會通過people.map(Person::name).filter{ it.startsWith("A") },但由于這2個操作符都會創(chuàng)建中間集合,那么上方的鏈?zhǔn)秸{(diào)用會創(chuàng)建2個列表而降低效率。

針對這種情況我們可以使用序列,而不是直接使用集合,如下所示。Sequence接口表示一個可以逐個枚舉元素的序列,Sequence只提供了一個方法iterator用來獲取元素值。

people.asSequence()
    .map(Person::name)
    .filter{ it.startsWith("A") }
    .toList()

對序列操作時,會將操作符依次應(yīng)用在每一個元素上,如果在遍歷完元素之前得到過了結(jié)果,那么之后的元素都不會發(fā)生變化。例如下面這個例子,處理到2時就得到了結(jié)果,之后的元素不會被處理,這就是惰性的含義。

listOf(1, 2, 3, 4)
    .asSequence
    .map{ it * it }
    .find{ it > 3 }

4.4 帶接收者的Lambda

with是一個接受兩個參數(shù)的函數(shù),第一個參數(shù)為Lambda的接收者,第二個參數(shù)為Lambda。在Lambda中可以通過this訪問接收者對象,一般來說this可省略,with的返回值就是Lambda的運(yùn)行結(jié)果。例如下方代碼就會返回"Hello World"的String。

with(StringBuilder()) {
    append("Hello ")
    append("World")
    toString()
}

apply與with的用法類似,區(qū)別是apply會返回作為實(shí)參傳遞給它的對象,就是接收者對象,例如下方代碼會返回一個StringBuilder。由于apply返回接收者對象的特性,可以將其用于對象初始化。

StringBuilder().apply {
    append("Hello ")
    append("World")
}

五、Kotlin的類型系統(tǒng)

5.1 可空性

Kotlin在避免空指針異常上做出了很多努力,其中最重要的一條就是支持可空類型,這意味著你可以在程序中指出哪些變量是可以為null的,而哪些變量是不允許的。如果一個變量允許為null,那么直接對它調(diào)用方法是不安全的,這樣的設(shè)計可以避免很多異常。例如,在Kotlin中使用var s: String = ""聲明的String變量不允許為空。如果需要一個可以為null的變量,那么需要在類型后面加上?,例如var ss: String? = null就聲明了一個可空的String變量。

5.1.1 安全調(diào)用運(yùn)算符"?."

對于可空的變量,需要使用安全調(diào)用運(yùn)算符"?.",它會將null檢查與和一次方法調(diào)用合并成一個操作。例如s?.toUpperCase()等價于if (s != null) s.toUpperCase() else null。也就是說,如果變量為null,"?."調(diào)用后的結(jié)果也為null。因此當(dāng)需要對可空類型鏈?zhǔn)秸{(diào)用時,可以采用stringBuilder?.append("...")?.append("...")這樣的形式。

5.1.2 通過"?:"提供默認(rèn)值

當(dāng)需要對null變量提供默認(rèn)值時可以使用"?:"操作符,例如val r: String = s ?: ""就使用空字符串代替了null:表示s不為null時使用s的值,為null時使用空字符串。由于return和throw這樣的操作也是表達(dá)式,可以跟在"?:"后面,當(dāng)"?:"左邊的值為null時直接返回或拋出異常。

5.1.3 安全類型轉(zhuǎn)換"as?"

當(dāng)在Kotlin中使用"as"進(jìn)行類型轉(zhuǎn)換時,如果類型不匹配會拋出ClassCastException異常,雖然可以用"is"來檢查類型,但是不夠簡潔。而使用"as?"可以進(jìn)行安全的類型轉(zhuǎn)換,如果類型不匹配則返回null而不是拋出異常。

5.1.4 "let"函數(shù)

處理可空表達(dá)式可以使用"let"函數(shù),例如當(dāng)前有個函數(shù)fun send(s: String)只接受不為空的參數(shù),如果有個可空類型的字符串則需要顯式判斷它是否為空再調(diào)用方法。
不過還有一種方式是通過"let"函數(shù):s?.let {send(it)},"let"的作用就是把調(diào)用它的對象作為lambda表達(dá)式的參數(shù),結(jié)合安全調(diào)用的方法,它能將調(diào)用let函數(shù)的可空對象轉(zhuǎn)化為非空類型。

5.1.5 延遲初始化的屬性

很多框架都會在實(shí)例創(chuàng)建之后用專門的方法來初始化對象,此時需要將屬性聲明為可空類型,因?yàn)閷傩栽诙x時都是為空的,之后每次使用這些屬性時都需要判空或者使用"?."進(jìn)行安全調(diào)用。
為了解決這個問題,可以使用lateinit修飾符將變量聲明為可以延遲初始化的。延遲初始化的屬性都是var,因?yàn)樾枰跇?gòu)造方法外修改它的值。

class Test {
    private lateinit var service: MyService

    fun setUp() {
        service = MyService()
    }
    
    fun testAction() {
        service.performAction()
    }
}

5.1.6 可空性與Java

Kotlin與Java是可以無縫兼容的,而Java中并不存在可空類型,此時Kotlin該如何調(diào)用Java代碼呢,是不是使用每個值之前都需要檢查是否為null?
其實(shí)Java也有時候包含了可空性的信息,例如@Nullable@NonNull這兩個注解就分別表示可空和不可空。但是大部分情況下,Kotlin都不知道Java類型的可空性信息,這種不清楚可空性的類型被稱為平臺類型,開發(fā)人員需要對平臺類型的操作負(fù)有全部責(zé)任,需要像在Java中一樣進(jìn)行判空。

5.2 基本數(shù)據(jù)類型

Java中對基本數(shù)據(jù)類型和引用類型進(jìn)行了區(qū)分,例如int這樣的基本數(shù)據(jù)類型直接存儲了它的值,而一個引用類型存儲了該對象的內(nèi)存地址引用,對于int類型提供了包裝類Integer作為引用類型。
不過Kotlin并不區(qū)分基本數(shù)據(jù)類型和包裝類型,對于整型永遠(yuǎn)使用Int,這并不意味著所有的Int都是對象:大多數(shù)情況下Kotlin中的Int會被編譯為Java中的int,只有作為泛型(用于集合中)或者使用可空類型時才會被編譯為包裝類型。

Kotlin與Java處理數(shù)字轉(zhuǎn)換的方式是不一樣的:Kotlin不會自動地把數(shù)字從一種類型轉(zhuǎn)換為另一種,即使是范圍更大的類型。例如當(dāng)前有val i = 1,使用val l: Long = i時會出現(xiàn)類型不匹配的錯誤,必須使用val l: Long = i.toLong()進(jìn)行顯式轉(zhuǎn)換。Kotlin規(guī)定所有的基本數(shù)據(jù)類型轉(zhuǎn)換都必須是顯式的,并為除了Boolean以外的基本數(shù)據(jù)類型都定義了轉(zhuǎn)換函數(shù)。

Kotlin使用"Any"和"Any?"作為根類型,這就像Java使用Object作為所有引用類型的超類型。當(dāng)使用Object時,必須要使用Integer這樣的裝箱類型來表示基本類型的值。而在Kotlin中,Any時所有類型的超類型,包括Int這樣的基礎(chǔ)數(shù)據(jù)類型。
和Java一樣,當(dāng)使用val v: Any = 1把基本數(shù)據(jù)類型的值賦值給Any時,變量會被自動裝箱。這里的Any表示變量不可為空,如果變量可能為null,則需要使用Any?類型,Any在底層對應(yīng)Java的Object類型。

5.3 集合與數(shù)組

在集合方面,Kotlin支持類型參數(shù)的可空性,例如List<Int?>是持有Int?類型值的列表,即可以持有Int或null,而List<Int>?指的是列表本身可能為空。針對List<Int?>這種持有可空類型的集合,Kotlin提供了標(biāo)準(zhǔn)庫函數(shù)filterNotNull()過濾空元素,一個List<Int?>類型的集合過濾后就不存在可空元素了,就變成了List<Int>類型。

Kotlin將集合的訪問和修改接口分開了,kotlin.collections.Collection接口可以執(zhí)行訪問集合的操作,但是沒有添加或移除元素的方法。使用kotlin.collections.MutableCollection接口可以修改集合中的元素。

只讀集合與可變集合的分離使得程序的可讀性更強(qiáng),如果函數(shù)接受Collection作為參數(shù),就代表它不會修改集合;如果函數(shù)接受MutableCollection作為參數(shù),則認(rèn)為它會修改數(shù)據(jù),如果你使用了集合作為組件狀態(tài)的一部分,可以考慮拷貝一份再傳遞給這樣的函數(shù)。需要注意的是,只讀集合不一定是不可變的,因?yàn)镸utableCollection接口繼承自Collection接口,某個只讀集合可能只是同一個集合眾多引用中的一個。

在Kotlin中只讀接口和可變接口的基本類型與java.util中的Java集合接口是平行的,可變接口直接對應(yīng)java.util包中的接口,而只讀版本缺少了所有產(chǎn)生改變的方法。下表展示了Kotlin創(chuàng)建集合的函數(shù)。

集合類型 只讀 可變
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, LinkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, LinkedMapOf, sortedMapOf

除了集合,Kotlin也支持創(chuàng)建數(shù)組,我們可以通過arrayOf創(chuàng)建一個數(shù)組,或者arrayOfNulls創(chuàng)建一個包含可空類型元素的數(shù)組。也可以通過val ss = Array<Int>(10) {...}這樣的方式,這里面的Lambda表達(dá)式用于創(chuàng)建每一個數(shù)組元素。不過這種方式下聲明的數(shù)組的元素類型都是裝箱類型(如Integer),如果想要創(chuàng)建基本數(shù)據(jù)類型的數(shù)組,可以使用IntArray, ByteArray, CharArray等,它們對應(yīng)Java中的int[]等基本數(shù)據(jù)類型數(shù)組。

val zeros = IntArray(5)
val zeros2 = IntArrayof(0, 0, 0, 0, 0)
val squares = IntArray(5) -> {i -> i * i}

六、運(yùn)算符重載等約定

6.1 基礎(chǔ)運(yùn)算符重載

Kotlin提供了一系列的運(yùn)算符重載方法,當(dāng)重寫這些方法后,就可以使用運(yùn)算符直接調(diào)用這些方法,可重載的二元運(yùn)算符如下所示。

表達(dá)式 函數(shù)名
a * b times
a / b div
a % b mod
a + b plus
a - b minus

舉個栗子,假設(shè)當(dāng)前有一個data類Point,重寫其plus(...)方法如下,對于重載運(yùn)算符的方法需要加上operator關(guān)鍵字,之后就可以通過"+"運(yùn)算符對兩個Point對象調(diào)用加法。
自定義類型的運(yùn)算符基本與標(biāo)準(zhǔn)數(shù)字類型的運(yùn)算符有著相同的優(yōu)先級,例如在a + b * c中,乘法始終在加法之前執(zhí)行。

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

// 調(diào)用時如下所示
val p1 = Point(1, 1)
val p2 = Point(2, 2)
println(p1 + p2)

很多情況下一個類沒有重載運(yùn)算符,那么需要使用擴(kuò)展方法的形式。

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

運(yùn)算符重載支持兩種不同的類型進(jìn)行運(yùn)算,例如可以重載Point的乘法操作表示縮放。值得注意的是,運(yùn)算符重載不支持交換律,例如下面這種情況只支持p * 1.5,如果希望還能使用1.5 * p,需要單獨(dú)定義operator fun Double.times(p: Point): Point方法。

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

除了二元運(yùn)算符,還可以重載以下一元運(yùn)算符。

表達(dá)式 函數(shù)名
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec

Kotlin還提供了"+=", "-="這一類復(fù)合賦值運(yùn)算符的重載,例如"+="的重載方法為plusAssign()。一般來說,plus()plusAssign()只要重載其中一個即可,如果重載了2個,在調(diào)用"+="時可能2個函數(shù)都適用,編譯器會報錯。

Kotlin標(biāo)準(zhǔn)庫中的集合支持這兩種方法,+和-運(yùn)算符總是返回一個新的集合。+=和-=用于可變集合時就始終修改它們,而用于只讀集合時,會返回一個修改過的副本。

6.2 重載比較運(yùn)算符

在Kotlin中可以對任何對象使用比較運(yùn)算符(==, !=, >, <等),當(dāng)使用==運(yùn)算符時,它會被轉(zhuǎn)換成equals()方法的調(diào)用。運(yùn)行a==b時,實(shí)際得到的是a?.equals(b) ?: (b == null)的結(jié)果。而Kotlin中的恒等運(yùn)算符===與java中的==完全相同,它用于檢查兩個參數(shù)是否是同一個對象的引用(如果是基本數(shù)據(jù)類型,檢查它們是否是相同的值)。

調(diào)用比較運(yùn)算符時會轉(zhuǎn)化為compareTo()方法的調(diào)用,該方法必須返回Int值,調(diào)用a >= b時返回a.compareTo(b) >= 0的結(jié)果。所有在Java中實(shí)現(xiàn)了Comparable接口的類,都可以在Kotlin中使用運(yùn)算符語法。

6.3 解構(gòu)聲明和組件函數(shù)

解構(gòu)聲明用于展開單個復(fù)合值,并使用它來初始化多個單獨(dú)的變量。實(shí)際上解構(gòu)聲明用到了Kotlin的約定原理,對于data類,編譯器為每個在主構(gòu)造方法中聲明的屬性生成一個componentN()函數(shù)(N代表第N個屬性,最多5個)。

val p = Point(10, 20)
val (x, y) = p

對于非data類,可以手動為它們生成componentN()函數(shù)。

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

解構(gòu)聲明也可以用于迭代map,這是因?yàn)镋ntry上有擴(kuò)展函數(shù)component1()component2(),分別返回Entry的key和value。

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) {
        // ......
    }
}

6.4 委托屬性

委托屬性表示將屬性的訪問器邏輯委托給了另一個對象,通過關(guān)鍵字by對其后的表達(dá)式求值來獲取這個對象,關(guān)鍵字by可以用于任何符合屬性委托約定規(guī)則的對象。

class Foo {
    var p: Type by Delegate()
}

按照約定,Delegate類必須具有getValue()setValue()方法,后者僅適用于可變屬性,當(dāng)調(diào)用Foo.p = newValue時實(shí)際調(diào)用了Delegate#setValue()方法。下方定義了委托類的2個方法,其中參數(shù)p表示接收屬性的實(shí)例,參數(shù)prop表示屬性本身,這個屬性的類型為KProperty。

class Delegate(var propValue: Int) {
    operator fun getValue(p: Type, prop: KProperty<*>): Int { }
    operator fun setValue(p: Type, prop: KProperty<*>, newValue: Int) { }
}

屬性委托經(jīng)常與lazy函數(shù)一起用于實(shí)現(xiàn)惰性初始化,例如val emails by lazy { loadEmails() },只有在emails變量被第一次使用時才會調(diào)用loadEmails()方法對其進(jìn)行初始化。lazy函數(shù)的參數(shù)是一個lambda,默認(rèn)情況下lazy函數(shù)是線程安全的,當(dāng)然也可以設(shè)置使用別的鎖或完全避開同步。

七、高階函數(shù):Lambda作為形參和返回值

7.1 高階函數(shù)

高階函數(shù)就是指以另一個函數(shù)作為參數(shù)或返回值的函數(shù),Kotlin中的函數(shù)可以通過lambda函數(shù)或函數(shù)引用來表示。函數(shù)類型的顯示聲明如下,Unit表示函數(shù)不返回任何有用的值。

val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = {...}

聲明函數(shù)時,編譯器可以推導(dǎo)出變量是否為函數(shù)類型,因此可以不聲明類型。

val sum = { x: Int, y: Int -> x + y }

7.1.1 將函數(shù)作為參數(shù)

下面聲明一個高階函數(shù),它以函數(shù)作為形參。此時需要顯示指定函數(shù)的類型,包括函數(shù)的參數(shù)類型以及返回值的類型。

fun advancedFunction(operation: (Int, Int) -> Int) {
    val result = operation(...)
}

如果實(shí)現(xiàn)一個基于String類型的filter函數(shù),其中傳入predicate函數(shù)來表示過濾規(guī)則。

fun String.filter(predicate: (Char) -> Boolean) : String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) {
            sb.append(element)
        }
    }
    return sb.toString()
}

Kotlin可以為函數(shù)參數(shù)指定一個默認(rèn)值表示默認(rèn)行為,該默認(rèn)值也可以為空。

// 指定默認(rèn)函數(shù)實(shí)現(xiàn)
fun function(callback : (() -> Unit) = {
        println("default invoke")}) {
    callback()
}

// 函數(shù)參數(shù)可以為空
fun function(callback : (() -> Unit)?) {
    callback?.invoke()
}

7.1.2 將函數(shù)作為返回值

將函數(shù)作為返回值時,需要指定該函數(shù)的類型,包括函數(shù)的參數(shù)類型和返回值類型。

fun getCalculator(type: Int): (Int) -> Int {
    if (type == 1) {
        return {value -> value * 10}
    } else {
        return {value -> value * 20}
    }
}

7.2 內(nèi)聯(lián)函數(shù):消除Lambda的運(yùn)行時開銷

當(dāng)一個函數(shù)被聲明為inline函數(shù)時,編譯器不會為其生成一個函數(shù),而是使用該函數(shù)的真實(shí)代碼替換每一次調(diào)用。以集合的filter函數(shù)為例,該函數(shù)被聲明為inline函數(shù),參數(shù)中傳遞的predicate函數(shù)在調(diào)用時也會被內(nèi)聯(lián)。

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

不過使用inline關(guān)鍵字只能提高帶lambda參數(shù)的函數(shù)的性能,因?yàn)椴粌?nèi)聯(lián)的話,lambda表達(dá)式會在調(diào)用時生成一個匿名類對象,從而影響性能。普通函數(shù)不需要使用inline去聲明,編譯器會進(jìn)行優(yōu)化。將函數(shù)聲明為inline時需要注意代碼的長度,如果代碼過長,將函數(shù)的字節(jié)碼拷貝到每一個調(diào)用點(diǎn)會極大地增大字節(jié)碼的長度。

7.3 高階函數(shù)中的return

如果在lambda中使用return,會從調(diào)用lambda的函數(shù)中返回,這樣的return語句稱為非局部返回。需要注意的是,只有當(dāng)該lambda函數(shù)為inline函數(shù)時才能從更外層的函數(shù)返回。
如果想要在lambda中返回,則需要為該lambda指定標(biāo)簽,然后return該標(biāo)簽,如下所示:

list.forEach label@ {
    if (it > 5) {
        return@label
    }
    // ...
}

7.4 Kotlin自帶高階函數(shù)

7.4.1 let函數(shù)

public inline fun <T, R> T.let(block: (T) -> R): R {}

根據(jù)let方法的定義,它接收一個類型為(T) -> R的方法作為參數(shù),在lambda中可以通過it來訪問調(diào)用let的對象。一般可以用let函數(shù)來進(jìn)行判空后的一些操作。

var str : String? = null
// str操作...
val len = str?.let {
    it.length
}

7.4.2 run函數(shù)

public inline fun <R> run(block: () -> R): R {}
public inline fun <T, R> T.run(block: T.() -> R): R {}

主要關(guān)注第二個run函數(shù),其傳入的block類型為T.() -> R,表示這是一個帶接受者的lambda,會將當(dāng)前的T對象攜帶到lambda中,即可在block中直接訪問T的屬性和方法。

val sb = StringBuilder()
// 如果傳入的參數(shù)可能為空, 則需使用?.操作符, 為空時不執(zhí)行l(wèi)ambda
val str = sb.run {
    append("abc")
    append("def")
    toString()
}

7.4.3 with函數(shù)

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {}

with函數(shù)與run函數(shù)的區(qū)別在于,它將receiver放入了參數(shù)中,因此調(diào)用的方式也有點(diǎn)不同。run函數(shù)可以在調(diào)用之前就進(jìn)行判空,但是with函數(shù)就要在lambda內(nèi)部判斷。

val len = with(sb) {
    this?.append("abc")
        ?.append("def")
        ?.length
}

7.4.4 apply函數(shù)

public inline fun <T> T.apply(block: T.() -> Unit): T {}

從函數(shù)定義來看,apply函數(shù)就是沒有返回值的with函數(shù),一般用于對象新建后的初始化,如下所示。

val test = Test().apply {
    param1 = true
    param2 = "test"
    param3 = 1
}

八、泛型

8.1 泛型類型參數(shù)

在使用泛型類型參數(shù)時可以使用類型參數(shù)限制,例如為類型形參指定上界,將類型指定為Number的子類:

fun <T : Number> List<T>.sum(): T

對于復(fù)雜的參數(shù),也可以為其指定多個約束,例如規(guī)定類型實(shí)參必須實(shí)現(xiàn)CharSequence和Appendable接口。

fun <T> ensure(seq T)
    where T: CharSequence, T: Appendable {
    // ......
}

而沒有指定上界的類型形參將會使用Any?這個默認(rèn)的上界,因此調(diào)用時需要使用?.操作符。如果想保證替換類型始終是非空類型,可以通過Any代替默認(rèn)的Any?作為上界。

8.2 泛型擦除與實(shí)化類型參數(shù)

Kotlin在不進(jìn)行特殊聲明的情況下,和Java一樣進(jìn)行了泛型擦除,因此下面的代碼是無法編譯的。

fun <T> isT(value : Any) = value is T

對于List<*>來說,在運(yùn)行時只能判斷當(dāng)前對象是否為List,而不能判斷具體的類型實(shí)參。例如if (value is List<String>)就無法被編譯。如果想判斷對象是否為List,那么可以使用if (value is List<*>)來檢查。

Kotlin的內(nèi)聯(lián)函數(shù)可以實(shí)化泛型,被實(shí)化的泛型需要用reified標(biāo)記。在編譯時,內(nèi)聯(lián)函數(shù)生成的字節(jié)碼會插入到函數(shù)調(diào)用的地方,而字節(jié)碼引用了具體的類,而不是類型參數(shù),因此不受泛型擦除的影響。

inline fun <reified T> isT(value : Any) = value is T

例如Kotlin標(biāo)準(zhǔn)庫中的filterIsInstance()方法用于返回指定類的實(shí)例,它的簡化實(shí)現(xiàn)如下。

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
}

還有一個例子是簡化Android中的startActivity,可以使用實(shí)化類型參數(shù)來代替activity類。

inline fun <reified T: Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

startActivity<DetailActivity>()

8.3 協(xié)變

一個協(xié)變類是一個泛型類,如Producer<T>,對于這種類來說,如果A是B的子類型,那么Producer<A>就是Producer<B>的子類型。如果要聲明類在某個類型參數(shù)上是可以協(xié)變的,在類型參數(shù)前加上out關(guān)鍵字即可。

interface Producer<out T> {
    fun produce(): T
}

在類成員的生命中,類型參數(shù)的使用分為in位置和out位置,如果函數(shù)把T當(dāng)做返回類型,那它在out位置;如果T用作函數(shù)參數(shù)的類型,那么它在in位置。

例如Kotlin中的List<Interface>接口,由于List是只讀的,所以它只有一個返回類型為T的get()方法,所以T在out位置。

interface List<out: T>: Collection<T> {
    operator fun get(index: Int): T
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內(nèi)容

  • Kotlin筆記 要理解Java與Kotlin的區(qū)別,就要從最根本的上來理解。Java是解釋型語言(邊解釋成二進(jìn)制...
    FFFSnow閱讀 1,009評論 0 0
  • kotlin優(yōu)勢簡潔(data class自帶get set equals hashCode toString c...
    紫鷹閱讀 285評論 0 1
  • 關(guān)鍵字 var val var name = "張三" name = "李四" //true name = 1/...
    SlideException閱讀 441評論 0 0
  • 初學(xué) Kotlin。隨手記一下不容易一下子記住的部分,只為在還不熟悉語言時不需要回頭翻整本書查找自己想要的內(nèi)容。 ...
    不再更新_閱讀 240評論 0 0
  • 第一次知道kotlin這個語言是在JakeWharton的這個dex-method-list 項(xiàng)目里,本來這個項(xiàng)目...
    dodomix閱讀 13,381評論 4 15