簡述: 今天迎來了Effective Kotlin系列的第四篇文章: 使用Sequence序列來優化大集合的頻繁操作.關於Sequence這個主題應該大家都不陌生,我寫過幾篇有關它的文章,可以說得上很詳細了。如果你對它的使用不太熟悉,歡迎檢視下面幾篇有關文章:
翻譯說明:
原標題: Effective Kotlin: Use Sequence for bigger collections with more than one processing step
原文地址: blog.kotlin-academy.com/effective-k…
原文作者: Marcin Moskala
開發者經常會忽略了Iterable
(迭代器)與Sequence
(序列)的區別。這其實也很正常,特別是當你去比較Iterable
與Sequence
的介面定義時,它們長得幾乎一樣。
interface Iterable<out T> {
operator fun iterator(): Iterator<T>
}
interface Sequence<out T> {
operator fun iterator(): Iterator<T>
}
複製程式碼
對比上面程式碼,你只能說出它們之間唯一不一樣就是介面名不同而已。但是Iterable
和Sequence
卻有著完全不同的用法,因此它們操作集合的函式的工作原理也是完全不同的。
序列是基於惰性的工作原理,因此處理序列的中間操作函式是不進行任何計算的。相反,它們會返回上一個中間操作處理後產生的新序列。所有這些一系列中間計算都將在終端操作執行中被確定,例如常見的終端操作toList
或count
.在另一方面,處理Iterable
的每個中間操作函式都是會返回一個新的集合。
fun main(args: Array<String>) {
val seq = sequenceOf(1,2,3)
print(seq.filter { it % 2 == 1 })
// Prints: kotlin.sequences.FilteringSequence@XXXXXXXX
print(seq.filter { it % 2 == 1 }.toList()) // Prints: [1, 3]
val list = listOf(1,2,3)
print(list.filter { it % 2 == 1 }) // Prints: [1, 3]
}
複製程式碼
序列的filter函式是一箇中間操作,所以它不會做任何的計算,而是經過新的處理步驟對序列進行了加工。最終的計算將在終端操作中完成,如toList函式
正因為這樣,處理操作的順序也是不同的。在處理序列過程中,我們通常會對單個元素進行一系列的整體操作,然後再對下一個元素做進行一系列的整體操作,直到處理完集合中所有元素為止。在處理iterable
過程中,我們是每一步操作都是針對整個集合進行,直到所有操作步驟執行完畢為止。
sequenceOf(1,2,3)
.filter { println("Filter $it, "); it % 2 == 1 }
.map { println("Map $it, "); it * 2 }
.toList()
// Prints: Filter 1, Map 1, Filter 2, Filter 3, Map 3,
listOf(1,2,3)
.filter { println("Filter $it, "); it % 2 == 1 }
.map { println("Map $it, "); it * 2 }
// Prints: Filter 1, Filter 2, Filter 3, Map 1, Map 3,
複製程式碼
由於這種惰性處理方式以及針對每個元素進行處理的順序,我們可以生成一個不定長的Sequence
序列
generateSequence(1) { it + 1 }
.map { it * 2 }
.take(10)
.forEach(::print)
// Prints: 2468101214161820
複製程式碼
對於具有一定經驗的Kotlin開發人員來說,這應該不陌生吧,但是在大多數文章或書籍中並沒有提及到有關序列一個更重要的知識點: 對於處理多個單一處理步驟的集合使用序列更高效
多個處理步驟是什麼意思?我的意思不僅僅是多個處理集合的單一函式。所以如果你比較這兩個函式:
fun singleStepListProcessing(): List<Product> {
return productsList.filter { it.bought }
}
fun singleStepSequenceProcessing(): List<Product> {
return productsList.asSequence()
.filter { it.bought }
.toList()
}
複製程式碼
你會注意到它們效能對比幾乎沒有任何差異,或者說處理簡單的集合速度更快(因為它是內聯的)。假如你對比了多個處理步驟的函式,比如先是filter
處理然後進行了map
處理的twoStepListProcessing
函式,那麼差異將是很明顯了。
fun twoStepListProcessing(): List<Double> {
return productsList
.filter { it.bought }
.map { it.price }
}
fun twoStepSequenceProcessing(): List<Double> {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.toList()
}
fun threeStepListProcessing(): Double {
return productsList
.filter { it.bought }
.map { it.price }
.average()
}
fun threeStepSequenceProcessing(): Double {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
複製程式碼
差異到底有多大呢? 讓我們對比一下基準測量出來的平均操作時間吧:
twoStepListProcessing 81 095 ns/op
twoStepSequenceProcessing 55 685 ns/op
twoStepListProcessingAndAcumulate 83 307 ns/op
twoStepSequenceProcessingAndAcumulate 6 928 ns/op
複製程式碼
當我們使用Sequences時, twoStepSequenceProcessing
函式明顯比twoStepListProcessing
函式處理集合速度要快很多。在這種情況下,優化的效率約為30%左右。
當我們分別使用Sequences
和Iterable
處理集合資料來獲得某個具體的數值而不是獲得一個新集合時,它們之間的效率差異將會變大。因為在這種情況下,序列根本就不需要建立任何中間集合。
來看一些典型的現實生活的例子,假設我們需要計算成年人購買該產品的平均價格:
fun productsListProcessing(): Double {
return clientsList
.filter { it.adult }
.flatMap { it.products }
.filter { it.bought }
.map { it.price }
.average()
}
fun productsSequenceProcessing(): Double {
return clientsList.asSequence()
.filter { it.adult }
.flatMap { it.products.asSequence() }
.filter { it.bought }
.map { it.price }
.average()
}
複製程式碼
這是對比結果:
SequencesBenchmark.productsListProcessing 712 434 ns/op
SequencesBenchmark.productsSequenceProcessing 572 012 ns/op
複製程式碼
我們大概提高了20%的優化效率,這比直接處理沒有使用flatMap的情況要低一點,但這已經是一個很大的提升了。
當你一次又一次對比測量的效能資料時,你會發現如下這個規律:
當我們有多個處理步驟時,使用序列處理集合通常比直接處理集合更快。
哪種場景下序列處理速度不會更快呢?
在有一些不常用的操作中使用序列處理速度並不會更快,因為我們需要完整地操作整個集合。來自Kotlin stdlib標準庫中的sorted
就是一個明顯的例子。
sorted
使用的最佳實現:它將Sequence
中的元素轉換到List
中,然後使用Java stdlib標準庫中的sort
函式進行排序操作。這個缺點就在於相比相同操作Collection
的過程,中間這個轉換過程是需要花費額外的時間的(儘管如果Iterable
不是Collection
或陣列,那麼差異就並不大,因為它轉換過程也是需要花費時間的)
如果Sequence序列有類似sort這樣的函式是有爭議的,因為它只是固定空間長度上的惰性,並且不能用於不定長的序列。之所以引進它是因為它是一種比較受歡迎的函式,並且以這種方式使用它更容易。但是Kotlin開發人員應該要記住,它不能用於不定長的序列
generateSequence(0) { it + 1 }.sorted().take(10).toList()
// 不定長的計算時間
generateSequence(0) { it + 1 }.take(10).sorted().toList()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼
sorted
是一個少見處理步驟的例子,它在Collection
上使用的效率會比Sequence
更高。當我們使用多個處理步驟和單個排序函式是,我們可以期待使用Sequence
序列處理的效能將得到提升。
// Took around 150 482 ns
fun productsSortAndProcessingList(): Double {
return productsList
.sortedBy { it.price }
.filter { it.bought }
.map { it.price }
.average()
}
// Took around 96 811 ns
fun productsSortAndProcessingSequence(): Double {
return productsList.asSequence()
.sortedBy { it.price }
.filter { it.bought }
.map { it.price }
.average()
}
複製程式碼
Java中的Stream(流)怎麼樣呢?
Java8中引入了流來處理集合。它們表現得看起來和Kotlin中的序列很像。
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
productsList.stream()
.filter { it.bought }
.mapToDouble { it.price }
.average()
.orElse(0.0)
複製程式碼
它們也都是基於惰性求值的原理並且在最後(終端)處理集合。Java中的流對於集合的處理效率幾乎和Kotlin中的序列處理集合一樣高。Java中的Stream流和Kotlin中的Sequence序列兩者最大的差別如下所示:
- Kotlin中
Sequence
序列有更多的操作符函式(因為它們可以被定義成擴充套件函式)並且它們的用法也相對更簡單(這是因為Kotlin的序列是已經在使用的Java流基礎上設計的 - 例如我們可以使用toList()
而不是collect(Collectors.toList())
) - Java的
Stream
流支援可以使用parallel
函式以並行模式使用Java流. 當我們擁有一臺具有多個核心的計算機時,這可以為我們帶來巨大的效能提升。 - Kotlin的
Sequence
序列可用於通用模組、Kotlin/JS模組和Kotlin/Native模組中。
除此之外,當我們不使用並行模式時,要說Java stream和 Kotlin sequence哪個更高效,這個真的很難說。
我的建議是僅僅將Java中的Stream
用於計算量較大的處理以及需要啟用並行模式的場景。否則其他一般場景使用Kotlin Stdlib標準庫中Sequence
序列,可以給你帶來相同效率並且操作函式使用起來也很簡單,程式碼更加簡潔。
譯者有話說
老實說這篇文章,好的地方在於原作者把Kotlin中的序列和Java 8中的流做了一個很好的對比,以及作者給出自己的使用建議以及針對效能效率都是通過實際基準測試結果進行對比的。但是唯一美中不足的是對於Kotlin中哪種場景下使用sequence更好,並沒有說的很清楚。關於這點我想補充一下:
- 1、資料集量級是足夠大,建議使用序列(Sequences)。
- 2、對資料集進行頻繁的資料操作,類似於多個操作符鏈式操作,建議使用序列(Sequences)
- 3、對於使用first{},last{}建議使用序列(Sequences)。補充一下,細心的小夥伴會發現當你對一個集合使用first{},last{}操作符的時候,我們IDE工具會提示你建議使用序列(Sequences) 代替 集合(Lists)
- 4、當僅僅只有map操作時,使用sequence
這裡只是簡單總結了幾點,具體詳細內容可參考之前三篇有關Kotlin中序列的文章。好了第四篇完美收工。
Kotlin系列文章,歡迎檢視:
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之探索高階函式中inline修飾符(三)
- [譯]Effective Kotlin系列之遇到多個構造器引數要考慮使用構建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)
原創系列:
- Jetbrains開發者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)
- JetBrains開發者日見聞(一)之Kotlin/Native 嚐鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(上篇)
- Kotlin的獨門祕籍Reified實化型別引數(下篇)
- 有關Kotlin屬性代理你需要知道的一切
- 淺談Kotlin中的Sequences原始碼解析
- 淺談Kotlin中集合和函式式API完全解析-上篇
- 淺談Kotlin語法篇之lambda編譯成位元組碼過程完全解析
- 淺談Kotlin語法篇之Lambda表示式完全解析
- 淺談Kotlin語法篇之擴充套件函式
- 淺談Kotlin語法篇之頂層函式、中綴呼叫、解構宣告
- 淺談Kotlin語法篇之如何讓函式更好地呼叫
- 淺談Kotlin語法篇之變數和常量
- 淺談Kotlin語法篇之基礎語法
翻譯系列:
- [譯]Kotlin中內聯類的自動裝箱和高效能探索(二)
- [譯]Kotlin中內聯類(inline class)完全解析(一)
- [譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)
- [譯]Kotlin泛型中何時該用型別形參約束?
- [譯] 一個簡單方式教你記住Kotlin的形參和實參
- [譯]Kotlin中是應該定義函式還是定義屬性?
- [譯]如何在你的Kotlin程式碼中移除所有的!!(非空斷言)
- [譯]掌握Kotlin中的標準庫函式: run、with、let、also和apply
- [譯]有關Kotlin型別別名(typealias)你需要知道的一切
- [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)?
- [譯]Kotlin中的龜(List)兔(Sequence)賽跑
實戰系列:
- 用Kotlin擼一個圖片壓縮外掛ImageSlimming-導學篇(一)
- 用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)
- 用Kotlin擼一個圖片壓縮外掛-實戰篇(三)
- 淺談Kotlin實戰篇之自定義View圖片圓角簡單應用
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~