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