一、Kotlin基礎
1.1 變量
在Kotlin中變量分為可變引用var和不可變引用val浸剩,val對應的是java中的final變量乒省。盡管val的引用地址是不可變的,但它指向的對象完全是可變的唇礁。在val變量的代碼塊執(zhí)行期間,它只能進行唯一一次初始化琢融,如果編譯器能確保只有唯一一條初始化語句會被執(zhí)行,可以根據(jù)條件使用不同的值來初始化纳令。
// 如果編譯器可以推導出類型平绩,那么不用顯示聲明類型笆搓。
val answer = 10
// 確保只有唯一一條初始化語句被執(zhí)行
val answer: Int
if (...) {
answer = 10
} else {
answer = 20
}
1.2 字符串模板
字符串模板指在String值中引用局部變量砚作,只需在變量名前加上$葫录,其效率等價于Java中的字符串拼接。除了直接引用變量面粮,還可以使用表達式,只需用{}包裹表達式即可婿脸。
// 引用變量
val s = "world"
print("hello $s")
// 引用表達式
val array = mutableListOf("hello", "world")
println("message is ${array[0]} and ${array[1]}")
其實編譯后的代碼創(chuàng)建了一個StringBuilder對象,并把常量部分和變量部分附加上去鸿脓。
1.3 類和屬性
在Kotlin中聲明一個實體類非常簡單抑钟,不需要像Java一樣聲明所有屬性和getter/setter方法。下方就是一個包含name屬性和isMarried屬性的Person實體類野哭,它沒有聲明類的訪問權限在塔,Kotlin中的默認可見性就是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)部實際調(diào)用person.getName()
方法;修改屬性是通過person.isMarried = true
,實際調(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個獨立的變量中鹦赎。
val map = TreeMap<String, String>()
// 添加元素至map......
for ((key, value) in map) {
// ......
}
1.5 異常處理
Kotlin中throw結(jié)構是一個表達式膊毁,能作為另一個表達式的一部分使用力图。如果number值正常祠斧,則percentage變量會被初始化,否則變量不會被初始化遣臼,直接拋出異常。
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException("......")
與之類似的是霹崎,異常處理的"try"也可以作為表達式默赂,下方的代碼在不出現(xiàn)異常時可以得到正確地值奖恰,出現(xiàn)異常時number會被賦值為null吊趾。
val number = try {
Integer.parseInt(str)
} catch (e: NumberFormatException) {
null
}
二瑟啃、函數(shù)
2.1 Kotlin函數(shù)基礎
在Kotlin中可以為函數(shù)的參數(shù)指定默認值湿颅,調(diào)用時可以省略這些有默認值的參數(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ù)名乓梨,并省略了兩個默認參數(shù)
collect(collection = strs, separator = " ")
由于Java沒有參數(shù)默認值的概念,當從Java中調(diào)用Kotlin函數(shù)時必須顯示指定所有參數(shù)值清酥。如果需要從Java代碼中頻繁地調(diào)用扶镀,并且希望能對Java的調(diào)用者更簡便,可以使用@JvmOverloads注解焰轻,它會讓編譯器為函數(shù)生成Java重載函數(shù)臭觉,調(diào)用時就可以從最后一個參數(shù)開始省略。
2.2 擴展函數(shù)與屬性
Kotlin可以為現(xiàn)有的類添加擴展函數(shù)辱志,用以平滑地與現(xiàn)有代碼集成蝠筑。例如可以為String類添加一個擴展函數(shù)來獲取字符串的最后一個字符。
package com.test
fun String.lastChar(): Char {
return this[this.length - 1]
}
定義后的擴展函數(shù)不會在整個項目范圍內(nèi)生效揩懒,在使用擴展函數(shù)時需要導入什乙。除了直接導入原函數(shù)外,還可以為擴展函數(shù)指定別名已球。
// 導入擴展函數(shù)
import com.test.lastChar
println("Kotlin".lastChar())
// 使用別名導入擴展函數(shù)
import com.test.lastChar as last
println("Kotlin".last())
擴展函數(shù)的本質(zhì)是靜態(tài)函數(shù)臣镣,因此不存在重寫。當從Java調(diào)用Kotlin的擴展函數(shù)時智亮,只需要通過文件名調(diào)用該擴展函數(shù)退疫,并將接受者對象作為第一個參數(shù)傳入即可。
擴展屬性實際是通過擴展類的API來訪問屬性鸽素,例如可以為StringBuilder類定義一個lastChar屬性表示最后一個字符褒繁,內(nèi)部定義getter和setter方法,在Kotlin中訪問屬性時實際調(diào)用了getter或setter方法馍忽。當從Java中訪問擴展屬性時棒坏,需要顯式調(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)用
當使用mapOf(1 to "one", 2 to "two", 3 to "three")
來創(chuàng)建map時,可以通過to表示鍵值的對應關系磨澡,to并不是關鍵字碗啄,而是表示一種函數(shù)調(diào)用。在聲明中綴調(diào)用時稳摄,需要使用infix修飾符稚字,一個to函數(shù)的聲明如下。
infix fun Any.to(other: Any) = Pair(this, other)
三厦酬、類胆描,對象和接口
3.1 類繼承結(jié)構
3.1.1 接口
Kotlin聲明接口的關鍵字與Java一樣都是interface,并且可以像Java8一樣為接口的方法提供默認實現(xiàn)仗阅。
interface clickable {
fun click()
fun showoff() = println("call showoff in clickable")
}
interface Focusable {
fun setFocusable()
fun showoff() = println("call showoff in focusable")
}
如果一個類實現(xiàn)了上面2個接口昌讲,那么必須實現(xiàn)showoff()
方法,否則編譯器會報錯减噪,如果想調(diào)用某一個接口的默認實現(xiàn)剧蚣,則需通過super指定該接口/父類。
override fun showoff() = super<Clickable>.showoff()
Java中的類默認是可以被繼承的旋廷,也可以重寫父類的方法鸠按,但是這也可能導致子類不正確行為。因此在Kotlin中饶碘,類和方法默認都是final的目尖,對可被繼承的類需要使用open修飾符,對每一個可被重寫的方法都要添加open修飾符扎运。
open class Button: Clickable() {
fun disable()
open fun animate() {...}
override fun click() {...}
}
3.1.2 可見性修飾符
Kotlin的可見性修飾符與Java一樣是public瑟曲、protected與private,但是默認的可見性有所不同:Java中為protected豪治,Kotlin中為public洞拨。需要注意的是,Kotlin中的protected成員只在類和子類中可見负拟,并且類的擴展函數(shù)不能訪問它的private和protected成員烦衣。
Kotlin還有個關鍵字internal表示“只在模塊內(nèi)部可見”,模塊指的是一組一起編譯的Kotlin文件,internal對模塊提供了細節(jié)實現(xiàn)的封裝花吟。在Java中這種封裝很容易被破壞秸歧,因為外部代碼可以將類定義到相同的包中從而得到訪問模塊私有聲明的權限。
3.1.3 內(nèi)部類
Java中的內(nèi)部類隱式持有外部類的引用衅澈,內(nèi)部類可以直接訪問外部類的屬性和方法渐排,如果不希望內(nèi)部類持有外部類的引用给赞,可以使用static修飾內(nèi)部類。
Kotlin中的默認內(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 構造方法
Kotlin可以用很簡單的方式聲明一個類和它的構造函數(shù),如下所示甩牺。User類包含nickname屬性蘑志,并且有一個以nickname為參數(shù)的構造函數(shù)累奈。
class User(val nickname: String)
這是聲明User類的簡寫贬派,如果不采用簡寫,那么是這樣的澎媒。
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
如果需要將構造函數(shù)私有化搞乏,可以使用private修飾構造器。
class Secretive private constructor() {}
在Java中使用private修飾構造器表示一個更通用的意思:這個類是一個靜態(tài)工具成員或是單例戒努。Kotlin針對這種特性提供了語言級別的功能请敦,例如我們之后會提到的lazy。
當為一個類(例如Android中的視圖)聲明多個構造函數(shù)時储玫,可以通過super或this關鍵字使該構造方法調(diào)用父類或者自己的構造方法侍筛,如下所示。
class CustomView: View {
// 調(diào)用當前類的構造方法
constructor(context: Context): this(context, null) {
// ......
}
// 調(diào)用父類的構造方法
constructor(context: Context, attr: AttributeSet): this(context, attr) {
// ......
}
}
3.3 實體類
在使用Java實現(xiàn)實體類時撒穷,我們一般需要重寫toString()
, equals()
和hashCode()
方法匣椰,現(xiàn)在來看看用Kotlin如何實現(xiàn),如下所示端礼。實現(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é)構調(diào)用add()
方法時蟀伸,會計算當前對象的hashcode判斷集合中是否已經(jīng)存在同樣hash的對象,如果存在相等的hashcode,會再判斷是否存在equal的對象望蜡。顯而易見唤崭,如果不重寫hashcode()
方法,向HashMap中不斷add()
相等的對象時脖律,由于它們的hashcode不相等谢肾,都會被添加到集合中。
每次實現(xiàn)一個實體類都要重寫這3個方法小泉,看起來有點麻煩芦疏。不過Kotlin提供了data關鍵字來修飾實體類,toString()
, equals()
和hashCode()
這3個方法會被自動創(chuàng)建微姊。
data class Client(val name: String, val postalCode: Int)
這種實體類的屬性都應該修飾為val酸茴,表示對象創(chuàng)建后不可變,只有不可變對象才能作為HashMap的Key兢交。而且在多線程編程中不可變對象也更有優(yōu)勢薪捍,因為不用擔心其他線程修改了它的狀態(tài)。
3.4 object關鍵字
通過object關鍵字可以很輕易地實現(xiàn)單例模式配喳,object表示定義一個類并創(chuàng)建一個對象酪穿,如下所示。由于這是單例晴裹,可以直接通過Singleton.function1()
這樣的形式調(diào)用單例的方法被济。在Java中則需使用Singleton.INSTANCE.function1()
調(diào)用。
object Singleton {
fun function1() {...}
}
當object修飾的類是嵌套類時涧团,在整個系統(tǒng)中同樣只具有一個實例只磷。
data class Person(val name: String) {
object NameComparator: Comparator<Person> {...}
}
使用companion object關鍵字可以構建伴生對象,伴生對象定義在類中泌绣,表示這是當前類的單例钮追,可以直接通過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基礎
Lambda編程可以將函數(shù)作為值使用,在調(diào)用方法時直接傳遞一段代碼作為形參仿滔。例如可以定義一個函數(shù)惠毁,表示求2個值的和。
val sumFun = {x: Int, y: Int -> x + y}
當需要獲得集合中滿足某個條件的對象時崎页,例如獲取Person集合中年級最大的對象鞠绰,可以使用people.maxByOrNull({ p: Person -> p.age })
。當Lambda是函數(shù)調(diào)用的最后一個實參時飒焦,可以省略括號蜈膨,而且可以用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變量甚至修改它們。當從lambda內(nèi)部訪問外部變量時胚宦,稱這些變量被lambda捕捉首有,就像下方例子的prefix。
fun printMsg(messages: Collection<String>, prefix: String) {
messages.forEach {
print("$prefix $it")
}
}
當捕捉final變量時枢劝,變量和lambda會被存儲并稍后執(zhí)行井联;當捕捉非final變量時,該變量會被封裝到一個特殊的包裝器呈野,隨后包裝器和lambda會被存儲并稍后執(zhí)行低矮。
4.2 集合的函數(shù)式API
假設有個整型集合val list = listOf(1, 2, 3, 4)
印叁,現(xiàn)在通過list來看集合API的功能被冒。
- filter: 對集合過濾,傳入的lambda表示過濾條件轮蜕。
list.filter{ it % 2 == 0 }
結(jié)果為{2, 4} - map: 對集合中的每一個元素運行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
除了上述這些基礎的集合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等操作符會返回一個集合對象桃纯。假設當前有一個需求,要得到people中名字以"A"開頭的名字列表披坏,你可能會通過people.map(Person::name).filter{ it.startsWith("A") }
态坦,但由于這2個操作符都會創(chuàng)建中間集合,那么上方的鏈式調(diào)用會創(chuàng)建2個列表而降低效率棒拂。
針對這種情況我們可以使用序列伞梯,而不是直接使用集合,如下所示帚屉。Sequence接口表示一個可以逐個枚舉元素的序列谜诫,Sequence只提供了一個方法iterator用來獲取元素值。
people.asSequence()
.map(Person::name)
.filter{ it.startsWith("A") }
.toList()
對序列操作時攻旦,會將操作符依次應用在每一個元素上喻旷,如果在遍歷完元素之前得到過了結(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的運行結(jié)果三热。例如下方代碼就會返回"Hello World"的String。
with(StringBuilder()) {
append("Hello ")
append("World")
toString()
}
apply與with的用法類似三幻,區(qū)別是apply會返回作為實參傳遞給它的對象就漾,就是接收者對象,例如下方代碼會返回一個StringBuilder赌髓。由于apply返回接收者對象的特性从藤,可以將其用于對象初始化催跪。
StringBuilder().apply {
append("Hello ")
append("World")
}
五、Kotlin的類型系統(tǒng)
5.1 可空性
Kotlin在避免空指針異常上做出了很多努力夷野,其中最重要的一條就是支持可空類型懊蒸,這意味著你可以在程序中指出哪些變量是可以為null的,而哪些變量是不允許的悯搔。如果一個變量允許為null骑丸,那么直接對它調(diào)用方法是不安全的,這樣的設計可以避免很多異常妒貌。例如通危,在Kotlin中使用var s: String = ""
聲明的String變量不允許為空。如果需要一個可以為null的變量灌曙,那么需要在類型后面加上?菊碟,例如var ss: String? = null
就聲明了一個可空的String變量。
5.1.1 安全調(diào)用運算符"?."
對于可空的變量在刺,需要使用安全調(diào)用運算符"?."逆害,它會將null檢查與和一次方法調(diào)用合并成一個操作。例如s?.toUpperCase()
等價于if (s != null) s.toUpperCase() else null
蚣驼。也就是說魄幕,如果變量為null,"?."調(diào)用后的結(jié)果也為null颖杏。因此當需要對可空類型鏈式調(diào)用時纯陨,可以采用stringBuilder?.append("...")?.append("...")
這樣的形式。
5.1.2 通過"?:"提供默認值
當需要對null變量提供默認值時可以使用"?:"操作符留储,例如val r: String = s ?: ""
就使用空字符串代替了null:表示s不為null時使用s的值翼抠,為null時使用空字符串。由于return和throw這樣的操作也是表達式欲鹏,可以跟在"?:"后面机久,當"?:"左邊的值為null時直接返回或拋出異常臭墨。
5.1.3 安全類型轉(zhuǎn)換"as?"
當在Kotlin中使用"as"進行類型轉(zhuǎn)換時赔嚎,如果類型不匹配會拋出ClassCastException異常,雖然可以用"is"來檢查類型胧弛,但是不夠簡潔尤误。而使用"as?"可以進行安全的類型轉(zhuǎn)換,如果類型不匹配則返回null而不是拋出異常结缚。
5.1.4 "let"函數(shù)
處理可空表達式可以使用"let"函數(shù)损晤,例如當前有個函數(shù)fun send(s: String)
只接受不為空的參數(shù),如果有個可空類型的字符串則需要顯式判斷它是否為空再調(diào)用方法红竭。
不過還有一種方式是通過"let"函數(shù):s?.let {send(it)}
尤勋,"let"的作用就是把調(diào)用它的對象作為lambda表達式的參數(shù)喘落,結(jié)合安全調(diào)用的方法,它能將調(diào)用let函數(shù)的可空對象轉(zhuǎn)化為非空類型最冰。
5.1.5 延遲初始化的屬性
很多框架都會在實例創(chuàng)建之后用專門的方法來初始化對象瘦棋,此時需要將屬性聲明為可空類型,因為屬性在定義時都是為空的暖哨,之后每次使用這些屬性時都需要判空或者使用"?."進行安全調(diào)用赌朋。
為了解決這個問題,可以使用lateinit修飾符將變量聲明為可以延遲初始化的篇裁。延遲初始化的屬性都是var沛慢,因為需要在構造方法外修改它的值。
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黍聂?
其實Java也有時候包含了可空性的信息伐庭,例如@Nullable
和@NonNull
這兩個注解就分別表示可空和不可空。但是大部分情況下分冈,Kotlin都不知道Java類型的可空性信息圾另,這種不清楚可空性的類型被稱為平臺類型,開發(fā)人員需要對平臺類型的操作負有全部責任雕沉,需要像在Java中一樣進行判空集乔。
5.2 基本數(shù)據(jù)類型
Java中對基本數(shù)據(jù)類型和引用類型進行了區(qū)分,例如int這樣的基本數(shù)據(jù)類型直接存儲了它的值坡椒,而一個引用類型存儲了該對象的內(nèi)存地址引用扰路,對于int類型提供了包裝類Integer作為引用類型。
不過Kotlin并不區(qū)分基本數(shù)據(jù)類型和包裝類型倔叼,對于整型永遠使用Int汗唱,這并不意味著所有的Int都是對象:大多數(shù)情況下Kotlin中的Int會被編譯為Java中的int,只有作為泛型(用于集合中)或者使用可空類型時才會被編譯為包裝類型丈攒。
Kotlin與Java處理數(shù)字轉(zhuǎn)換的方式是不一樣的:Kotlin不會自動地把數(shù)字從一種類型轉(zhuǎn)換為另一種哩罪,即使是范圍更大的類型。例如當前有val i = 1
巡验,使用val l: Long = i
時會出現(xiàn)類型不匹配的錯誤际插,必須使用val l: Long = i.toLong()
進行顯式轉(zhuǎn)換。Kotlin規(guī)定所有的基本數(shù)據(jù)類型轉(zhuǎn)換都必須是顯式的显设,并為除了Boolean以外的基本數(shù)據(jù)類型都定義了轉(zhuǎn)換函數(shù)框弛。
Kotlin使用"Any"和"Any?"作為根類型,這就像Java使用Object作為所有引用類型的超類型捕捂。當使用Object時瑟枫,必須要使用Integer這樣的裝箱類型來表示基本類型的值斗搞。而在Kotlin中,Any時所有類型的超類型慷妙,包括Int這樣的基礎數(shù)據(jù)類型榜旦。
和Java一樣,當使用val v: Any = 1
把基本數(shù)據(jù)類型的值賦值給Any時景殷,變量會被自動裝箱溅呢。這里的Any表示變量不可為空,如果變量可能為null猿挚,則需要使用Any?類型咐旧,Any在底層對應Java的Object類型。
5.3 集合與數(shù)組
在集合方面绩蜻,Kotlin支持類型參數(shù)的可空性铣墨,例如List<Int?>
是持有Int?類型值的列表,即可以持有Int或null办绝,而List<Int>?
指的是列表本身可能為空伊约。針對List<Int?>
這種持有可空類型的集合,Kotlin提供了標準庫函數(shù)filterNotNull()
過濾空元素孕蝉,一個List<Int?>
類型的集合過濾后就不存在可空元素了屡律,就變成了List<Int>
類型。
Kotlin將集合的訪問和修改接口分開了降淮,kotlin.collections.Collection
接口可以執(zhí)行訪問集合的操作超埋,但是沒有添加或移除元素的方法。使用kotlin.collections.MutableCollection
接口可以修改集合中的元素佳鳖。
只讀集合與可變集合的分離使得程序的可讀性更強霍殴,如果函數(shù)接受Collection作為參數(shù),就代表它不會修改集合系吩;如果函數(shù)接受MutableCollection作為參數(shù)来庭,則認為它會修改數(shù)據(jù),如果你使用了集合作為組件狀態(tài)的一部分穿挨,可以考慮拷貝一份再傳遞給這樣的函數(shù)月弛。需要注意的是,只讀集合不一定是不可變的絮蒿,因為MutableCollection接口繼承自Collection接口尊搬,某個只讀集合可能只是同一個集合眾多引用中的一個。
在Kotlin中只讀接口和可變接口的基本類型與java.util
中的Java集合接口是平行的土涝,可變接口直接對應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表達式用于創(chuàng)建每一個數(shù)組元素溯祸。不過這種方式下聲明的數(shù)組的元素類型都是裝箱類型(如Integer)肢专,如果想要創(chuàng)建基本數(shù)據(jù)類型的數(shù)組,可以使用IntArray
, ByteArray
, CharArray
等焦辅,它們對應Java中的int[]等基本數(shù)據(jù)類型數(shù)組博杖。
val zeros = IntArray(5)
val zeros2 = IntArrayof(0, 0, 0, 0, 0)
val squares = IntArray(5) -> {i -> i * i}
六、運算符重載等約定
6.1 基礎運算符重載
Kotlin提供了一系列的運算符重載方法筷登,當重寫這些方法后剃根,就可以使用運算符直接調(diào)用這些方法,可重載的二元運算符如下所示前方。
表達式 | 函數(shù)名 |
---|---|
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
舉個栗子狈醉,假設當前有一個data類Point,重寫其plus(...)
方法如下惠险,對于重載運算符的方法需要加上operator關鍵字苗傅,之后就可以通過"+"運算符對兩個Point對象調(diào)用加法。
自定義類型的運算符基本與標準數(shù)字類型的運算符有著相同的優(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)
很多情況下一個類沒有重載運算符趣竣,那么需要使用擴展方法的形式摇庙。
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
運算符重載支持兩種不同的類型進行運算,例如可以重載Point的乘法操作表示縮放遥缕。值得注意的是卫袒,運算符重載不支持交換律,例如下面這種情況只支持p * 1.5
单匣,如果希望還能使用1.5 * p
夕凝,需要單獨定義operator fun Double.times(p: Point): Point
方法。
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
除了二元運算符户秤,還可以重載以下一元運算符码秉。
表達式 | 函數(shù)名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
Kotlin還提供了"+=", "-="這一類復合賦值運算符的重載,例如"+="的重載方法為plusAssign()
鸡号。一般來說转砖,plus()
和plusAssign()
只要重載其中一個即可,如果重載了2個,在調(diào)用"+="時可能2個函數(shù)都適用府蔗,編譯器會報錯晋控。
Kotlin標準庫中的集合支持這兩種方法,+和-運算符總是返回一個新的集合姓赤。+=和-=用于可變集合時就始終修改它們赡译,而用于只讀集合時,會返回一個修改過的副本不铆。
6.2 重載比較運算符
在Kotlin中可以對任何對象使用比較運算符(==, !=, >, <等)蝌焚,當使用==運算符時,它會被轉(zhuǎn)換成equals()
方法的調(diào)用誓斥。運行a==b
時只洒,實際得到的是a?.equals(b) ?: (b == null)
的結(jié)果。而Kotlin中的恒等運算符===與java中的==完全相同岖食,它用于檢查兩個參數(shù)是否是同一個對象的引用(如果是基本數(shù)據(jù)類型红碑,檢查它們是否是相同的值)。
調(diào)用比較運算符時會轉(zhuǎn)化為compareTo()
方法的調(diào)用泡垃,該方法必須返回Int值析珊,調(diào)用a >= b
時返回a.compareTo(b) >= 0
的結(jié)果。所有在Java中實現(xiàn)了Comparable接口的類蔑穴,都可以在Kotlin中使用運算符語法忠寻。
6.3 解構聲明和組件函數(shù)
解構聲明用于展開單個復合值,并使用它來初始化多個單獨的變量存和。實際上解構聲明用到了Kotlin的約定原理奕剃,對于data類,編譯器為每個在主構造方法中聲明的屬性生成一個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
}
解構聲明也可以用于迭代map操软,這是因為Entry上有擴展函數(shù)component1()
和component2()
,分別返回Entry的key和value宪祥。
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
// ......
}
}
6.4 委托屬性
委托屬性表示將屬性的訪問器邏輯委托給了另一個對象聂薪,通過關鍵字by對其后的表達式求值來獲取這個對象,關鍵字by可以用于任何符合屬性委托約定規(guī)則的對象蝗羊。
class Foo {
var p: Type by Delegate()
}
按照約定藏澳,Delegate類必須具有getValue()
和setValue()
方法,后者僅適用于可變屬性耀找,當調(diào)用Foo.p = newValue
時實際調(diào)用了Delegate#setValue()
方法翔悠。下方定義了委托類的2個方法,其中參數(shù)p表示接收屬性的實例,參數(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ù)一起用于實現(xiàn)惰性初始化复罐,例如val emails by lazy { loadEmails() }
涝登,只有在emails變量被第一次使用時才會調(diào)用loadEmails()
方法對其進行初始化。lazy函數(shù)的參數(shù)是一個lambda效诅,默認情況下lazy函數(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ù)時,編譯器可以推導出變量是否為函數(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(...)
}
如果實現(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ù)指定一個默認值表示默認行為,該默認值也可以為空缩赛。
// 指定默認函數(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的運行時開銷
當一個函數(shù)被聲明為inline函數(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關鍵字只能提高帶lambda參數(shù)的函數(shù)的性能扇丛,因為不內(nèi)聯(lián)的話,lambda表達式會在調(diào)用時生成一個匿名類對象尉辑,從而影響性能帆精。普通函數(shù)不需要使用inline去聲明,編譯器會進行優(yōu)化。將函數(shù)聲明為inline時需要注意代碼的長度卓练,如果代碼過長隘蝎,將函數(shù)的字節(jié)碼拷貝到每一個調(diào)用點會極大地增大字節(jié)碼的長度。
7.3 高階函數(shù)中的return
如果在lambda中使用return襟企,會從調(diào)用lambda的函數(shù)中返回嘱么,這樣的return語句稱為非局部返回。需要注意的是顽悼,只有當該lambda函數(shù)為inline函數(shù)時才能從更外層的函數(shù)返回曼振。
如果想要在lambda中返回,則需要為該lambda指定標簽蔚龙,然后return該標簽冰评,如下所示:
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ù)來進行判空后的一些操作。
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 {}
主要關注第二個run函數(shù)坑填,其傳入的block類型為T.() -> R抛人,表示這是一個帶接受者的lambda,會將當前的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)用的方式也有點不同盅惜。run函數(shù)可以在調(diào)用之前就進行判空,但是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
對于復雜的參數(shù),也可以為其指定多個約束,例如規(guī)定類型實參必須實現(xiàn)CharSequence和Appendable接口。
fun <T> ensure(seq T)
where T: CharSequence, T: Appendable {
// ......
}
而沒有指定上界的類型形參將會使用Any?這個默認的上界拜轨,因此調(diào)用時需要使用?.操作符。如果想保證替換類型始終是非空類型盒发,可以通過Any代替默認的Any?作為上界例嘱。
8.2 泛型擦除與實化類型參數(shù)
Kotlin在不進行特殊聲明的情況下,和Java一樣進行了泛型擦除宁舰,因此下面的代碼是無法編譯的拼卵。
fun <T> isT(value : Any) = value is T
對于List<*>來說,在運行時只能判斷當前對象是否為List蛮艰,而不能判斷具體的類型實參腋腮。例如if (value is List<String>)
就無法被編譯。如果想判斷對象是否為List印荔,那么可以使用if (value is List<*>)
來檢查低葫。
Kotlin的內(nèi)聯(lián)函數(shù)可以實化泛型详羡,被實化的泛型需要用reified標記仍律。在編譯時,內(nèi)聯(lián)函數(shù)生成的字節(jié)碼會插入到函數(shù)調(diào)用的地方实柠,而字節(jié)碼引用了具體的類水泉,而不是類型參數(shù),因此不受泛型擦除的影響窒盐。
inline fun <reified T> isT(value : Any) = value is T
例如Kotlin標準庫中的filterIsInstance()
方法用于返回指定類的實例草则,它的簡化實現(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ù)來代替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關鍵字即可。
interface Producer<out T> {
fun produce(): T
}
在類成員的生命中夫壁,類型參數(shù)的使用分為in位置和out位置拾枣,如果函數(shù)把T當做返回類型,那它在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
}