前言
本文將介紹 Kotlin 中 序列(Sequence)的概念及使用,並介紹該惰性集合操作對集合鏈式呼叫效能優化背後的原理。
閱讀本文大概需要 5 分鐘,寫作本文大概消耗 7 個小時
目錄
序列(Sequence)
概念
在使用 Kotlin 集合操作符進行鏈式呼叫時,例如 map
和 filter
時,都會在函式內部建立中間集合,比如下面的例子,使用 map 和 filter 在 User 集合中篩選出性別為男的成員,返回結果是一個集合。
users.map(User :: sex)
.filter {it.sex.equals("male")}
複製程式碼
序列的用法
序列的用法很簡單,只需要再集合後新增asSeqence()
函式即可
users.asSequence()
.map(User :: sex)
.filter {it.sex.equals("male")}
複製程式碼
這裡插播一個概念,其中 User :: user
是成員引用,具體介紹如下
成員引用(Member References)
概念
成員引用可以使你方便的呼叫某個類的成員,這個成員包括對應類的屬性或方法.雙冒號前的是被引用的類,雙冒號後是需要返回的屬性或方法名,如下所示是返回 User 成員的 sex 屬性:
User :: sex
複製程式碼
成員引用可以方便的賦值給其他變數或函式,例如上述尋找性別為 male 的例子,也可以用稍微複雜的寫法,如下:
users.map(user : User -> user.sex)
.filter {it.sex.equals("male")}
複製程式碼
可見成員引用的寫法可讀性更強。
再談序列
讓我們回到序列介紹。上文提到使用 map
和 filter
時,都會在函式內部建立中間集合,這會導致一個問題,如果源列表,就是 users 中元素特別多,集合的鏈式處理會變得十分低效,原因是建立了多次中間集合。而如果先將待處理集合通過 asSequence()
方法轉換為序列,再進行 map
和 filter
操作,就會變得十分高效。對於是否使用序列進行集合操作,有幾個前提,如果使用不當,反而會造成效能損失。這裡總結一下使用場景:
序列效能測試
上文提到,是否使用序列的條件之一是處理大量資料,那麼這個閾值究竟是多少?下面來進行一個效能測試,構造一個商品列表,其中每個商品包含商品名和價格兩個屬性,現在要求出對每個商品的價格加價 100 後,價格為奇數 的商品的個數,這裡用到了 count()
方法,作用是求得集合內滿足 count 條件的元素的個數,程式碼如下:
/**
* 商品類
*/
data class Commodity(var name: String, var price: String)
複製程式碼
import java.util.*
fun main(args: Array<String>) {
val commodityList = ArrayList<Commodity>()
for (i in 0..1000000) {
val goods = Commodity("商品 $i", i * 5)
commodityList.add(goods)
}
val startTime = System.currentTimeMillis()
commodityList
.asSequence() // 使用此函式代表使用 Kotlin 序列功能
.map { it.price + 100 }
.count { it % 2 != 0 }
println("consume time is ${System.currentTimeMillis() - startTime} ms")
}
複製程式碼
測試結果折線圖如下,其中橫座標為集合內元素的個數,縱座標為程式碼執行時間,橙色線代表未使用序列,藍色線代表使用序列:
表格對比如下:由圖可得出如下結論:
- 上文提到的閾值大致為「一百萬」個元素,大於該閾值時,使用序列大致能帶來 90 % 的效能提升
- 在小於「十萬」個元素時,使用序列反而會造成效能下降
為什麼序列會提高集合操作的效能?
- 序列對集合的操作是惰性的。
- 不需要額外的建立中間集合儲存鏈式操作的中間結果
對於第一點,惰性這個詞可能給人帶來迷惑,這和使用序列後,集合的計算方式有關,下面來介紹這一點,在下面這個例子中,你需要在一個整型資料集合,將每個數乘以 2 ,並找出集合中小於 10 的第一個元素。
var result = listOf(2,4,6,8,10).asSequence
.map(it * 2)
.find(it > 10)
複製程式碼
find()
方法的作用是在集合中查詢滿足條件的第一個元素,並返回查詢到的值。下圖是 Kotlin 使用序列進行處理
由圖可知,所謂惰性,就是在使用 map
或 filter
等操作符的時候,在程式碼的執行順序上,不會優先遍歷所有的集合,即偷個懶,先對集合中第一個元素進行 map 和 filter, 然後對該元素執行 find 操作,發現滿足 find 的條件,就立刻返回結果,由此可見,在有 map 和 find 或 last 等操作符組合時,序列的效能優化能發生最大的作用。
小結
- 在進行集合操作時,使用序列操作符,可以降低集合操作的時間和佔用的空間,降低時間的原因是惰性操作,降低空間佔用的原因是序列在執行操作時不會建立中間集合。
- 序列操作雖好,但也要視業務場景決定是否使用,否則反而會降低效率。
參考文獻
- 《實戰 Kotlin 》
- Kotlin系列之序列(Sequences)原始碼完全解析