Kotlin Collection VS Kotlin Sequence VS Java Stream

pexels-sobhan-joodi-3215050.jpg

一. 集合中的函數(shù)式 API

雖然 Kotlin Collection 中的函數(shù)式 API 類似于 Java 8 Stream 中的 API。但是 Kotlin 的集合跟 Java 的集合并不一致。

Kolin 的集合分為可變集合(mutable collection)和不可變集合(immutable collection)。不可變集合是 List、Set、Map,它們是只讀類型,不能對(duì)集合進(jìn)行修改??勺兗鲜?MutableList、MutableSet、MutableMap,它們是支持讀寫的類型,能夠?qū)线M(jìn)行修改的操作。

Kotlin 集合中的函數(shù)式 API 跟大部分支持 Lambda 語(yǔ)言的函數(shù)式 API 都類似。下面僅以 filter、map、flatMap 三個(gè)函數(shù)為例,演示使用集合的高階函數(shù)。

1.1 filter 的使用

過(guò)濾集合中大于10的數(shù)字,并把它打印出來(lái)。

    listOf(5, 12, 8, 33)   // 創(chuàng)建list集合
            .filter { it > 10 }
            .forEach(::println)

執(zhí)行結(jié)果:

12
33

::println 是方法引用(Method Reference),它是簡(jiǎn)化的 Lambda 表達(dá)式。

上述代碼等價(jià)于下面的代碼:

    listOf(5, 12, 8, 33)
            .filter { it > 10 }
            .forEach{ println(it) }

1.2 map 的使用

將集合中的字符串都轉(zhuǎn)換成大寫,并打印出來(lái)。

    listOf("java","kotlin","scala","groovy")
            .map { it.toUpperCase() }
            .forEach(::println)

執(zhí)行結(jié)果:

JAVA
KOTLIN
SCALA
GROOVY

1.3 flatMap 的使用

遍歷所有的元素,為每一個(gè)創(chuàng)建一個(gè)集合,最后把所有的集合放在一個(gè)集合中。

    val newList = listOf(5, 12, 8, 33)
            .flatMap {
                listOf(it, it + 1)
            }

    println(newList)

執(zhí)行結(jié)果:

[5, 6, 12, 13, 8, 9, 33, 34]

二. Sequence

序列(Sequence)是 Kotlin 標(biāo)準(zhǔn)庫(kù)提供的另一種容器類型。序列與集合有相同的函數(shù) API,卻采用不同的實(shí)現(xiàn)方式。

其實(shí),Kotlin 的 Sequence 更類似于 Java 8 的 Stream,二者都是延遲執(zhí)行。Kotlin 的集合轉(zhuǎn)換成 Sequence 只需使用asSequence()方法。

    listOf(5, 12, 8, 33)
            .asSequence()
            .filter { it > 10 }
            .forEach(::println)

亦或者使用sequenceOf()來(lái)直接創(chuàng)建新的 Sequence:

    sequenceOf(5, 12, 8, 33) // 創(chuàng)建sequence
            .filter { it>10 }
            .forEach (::println)

在 Kotlin 1.2.70 的 release note 上曾說(shuō)明:

使用 Sequence 有助于避免不必要的臨時(shí)分配開銷,并且可以顯著提高復(fù)雜處理 PipeLines 的性能。

下面編寫一個(gè)例子來(lái)證實(shí)這個(gè)說(shuō)法:

@BenchmarkMode(Mode.Throughput) // 基準(zhǔn)測(cè)試的模式,采用整體吞吐量的模式
@Warmup(iterations = 3) // 預(yù)熱次數(shù)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 測(cè)試參數(shù),iterations = 10 表示進(jìn)行10輪測(cè)試
@Threads(8) // 每個(gè)進(jìn)程中的測(cè)試線程數(shù)
@Fork(2)  // 進(jìn)行 fork 的次數(shù),表示 JMH 會(huì) fork 出兩個(gè)進(jìn)程來(lái)進(jìn)行測(cè)試
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基準(zhǔn)測(cè)試結(jié)果的時(shí)間類型
open class SequenceBenchmark {

    @Benchmark
    fun testSequence():Int {
        return sequenceOf(1,2,3,4,5,6,7,8,9,10)
                .map{ it * 2 }
                .filter { it % 3  == 0 }
                .map{ it+1 }
                .sum()
    }

    @Benchmark
    fun testList():Int {
        return listOf(1,2,3,4,5,6,7,8,9,10)
                .map{ it * 2 }
                .filter { it % 3  == 0 }
                .map{ it+1 }
                .sum()
    }
}

fun main() {
    val options = OptionsBuilder()
            .include(SequenceBenchmark::class.java.simpleName)
            .output("benchmark_sequence.log")
            .build()
    Runner(options).run()
}

通過(guò)基準(zhǔn)測(cè)試得到如下的結(jié)果:

# Run complete. Total time: 00:05:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                        Mode  Cnt      Score     Error   Units
SequenceBenchmark.testList      thrpt   20  15924.272 ± 305.825  ops/ms
SequenceBenchmark.testSequence  thrpt   20  23099.938 ± 515.524  ops/ms

上述例子使用 OpenJDK 提供的基準(zhǔn)測(cè)試工具 JMH 進(jìn)行測(cè)試,它可以在方法層面進(jìn)行基準(zhǔn)測(cè)試。上述例子的結(jié)果表明,在多次鏈?zhǔn)秸{(diào)用時(shí) Sequence 比起 List 具有更高的效率。

這是因?yàn)榧显谔幚砻總€(gè)步驟時(shí)都會(huì)返回一個(gè)新集合,Sequence 不會(huì)在每個(gè)處理步驟中創(chuàng)建集合。對(duì)于數(shù)據(jù)量比較大時(shí),應(yīng)該選擇 Sequence。

三. Sequence VS Stream

Sequence 和 Stream 都使用的是惰性求值。

在編程語(yǔ)言理論中,惰性求值(英語(yǔ):Lazy Evaluation),又譯為惰性計(jì)算、懶惰求值,也稱為傳需求調(diào)用(call-by-need),是一個(gè)計(jì)算機(jī)編程中的一個(gè)概念,目的是要最小化計(jì)算機(jī)要做的工作。它有兩個(gè)相關(guān)而又有區(qū)別的含意,可以表示為“延遲求值”和“最小化求值”。除可以得到性能的提升外,惰性計(jì)算的最重要的好處是它可以構(gòu)造一個(gè)無(wú)限的數(shù)據(jù)類型。

下面列舉了 Sequence 和 Stream 的一些區(qū)別:

特性對(duì)比 Sequence Stream
autoboxing 會(huì)發(fā)生自動(dòng)裝箱 對(duì)于原始類型可以避免自動(dòng)裝箱
parallelism 不支持 支持
跨平臺(tái) 支持 Kotlin/JVM、Kotlin/JS、Kotlin/Native 等多平臺(tái) 只能在 Kotlin/JVM 平臺(tái)使用,并且 jvm 版本需要>=8
易用性 更簡(jiǎn)潔、支持更多的功能 使用 Collectors 進(jìn)行終端操作會(huì)使 Stream 更加冗長(zhǎng)。
性能 大多數(shù)終端操作符是 inline 函數(shù) 對(duì)于值可能不存在的情況,Sequence 支持可為空的類型,而 Stream 會(huì)創(chuàng)建 Optional包裝器。因此會(huì)多一步的對(duì)象創(chuàng)建。

從易用性、性能角度來(lái)看,如果要從 Sequence 和 Stream 中作出選擇的話,本人更加偏向 Sequence。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容