淺談Kotlin中的Sequences源碼解析(十)

簡述: 很久沒有發佈原創文章,一如既往,今天開始Kotlin淺談系列的第十講,一塊兒來探索Kotlin中的序列。序列(Sequences)其實是對應Java8中的Stream的翻版。從以前博客能夠了解到Kotlin定義了不少操做集合的API,沒錯這些函數照樣適用於序列(Sequences),並且序列操做在性能方面優於集合操做.並且經過以前函數式API的源碼中能夠看出它們會建立不少中間集合,每一個操做符都會開闢內存來存儲中間數據集,然而這些在序列中就不須要。讓咱們一塊兒來看看這篇博客內容:java

  • 一、爲何須要序列(Sequences)?
  • 二、什麼是序列(Sequences)?
  • 三、怎麼建立序列(Sequences)?
  • 四、序列(Sequences)操做和集合操做性能對比
  • 五、序列(Sequences)性能優化的原理
  • 六、序列(Sequences)原理源碼徹底解析

一、爲何須要序列(Sequences)?

咱們通常在Kotlin中處理數據集都是集合,以及使用集合中一些函數式操做符API,咱們不多去將數據集轉換成序列再去作集合操做。這是由於咱們通常接觸的數據量級比較小,使用集合和序列沒什麼差異,讓咱們一塊兒來看個例子,你就會明白使用序列的意義了。設計模式

//不使用Sequences序列,使用普通的集合操做
fun computeRunTime(action: (() -> Unit)?) {
  val startTime = System.currentTimeMillis()
  action?.invoke()
  println("the code run time is ${System.currentTimeMillis() - startTime}")
}

fun main(args: Array<String>) = computeRunTime {
  (0..10000000)
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 10 }
        .run {
            println("by using list way, result is : $this")
        }
}
複製代碼

運行結果:性能優化

//轉化成Sequences序列,使用序列操做
fun computeRunTime(action: (() -> Unit)?) {
    val startTime = System.currentTimeMillis()
    action?.invoke()
    println("the code run time is ${System.currentTimeMillis() - startTime}")
}

fun main(args: Array<String>) = computeRunTime {
    (0..10000000)
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 10 }
        .run {
            println("by using sequences way, result is : $this")
        }
}
複製代碼

運行結果:ide

經過以上同一個功能實現,使用普通集合操做和轉化成序列後再作操做的運行時間差距不只一點點,也就對應着兩種實現方式在數據集量級比較大的狀況下,性能差別也是很大的。這樣應該知道爲何咱們須要使用Sequences序列了吧。函數

二、什麼是序列(Sequences)?

序列操做又被稱之爲惰性集合操做,Sequences序列接口強大在於其操做的實現方式。序列中的元素求值都是惰性的,因此能夠更加高效使用序列來對數據集中的元素進行鏈式操做(映射、過濾、變換等),而不須要像普通集合那樣,每進行一次數據操做,都必需要開闢新的內存來存儲中間結果,而實際上絕大多數的數據集合操做的需求關注點在於最後的結果而不是中間的過程,工具

序列是在Kotlin中操做數據集的另外一種選擇,它和Java8中新增的Stream很像,在Java8中咱們能夠把一個數據集合轉換成Stream,而後再對Stream進行數據操做(映射、過濾、變換等),序列(Sequences)能夠說是用於優化集合在一些特殊場景下的工具。可是它不是用來替代集合,準確來講它起到是一個互補的做用。源碼分析

序列操做分爲兩大類:post

  • 一、中間操做

序列的中間操做始終都是惰性的,一次中間操做返回的都是一個序列(Sequences),產生的新序列內部知道如何變換原始序列中的元素。怎樣說明序列的中間操做是惰性的呢?一塊兒來看個例子:性能

fun main(args: Array<String>) {
    (0..6)
        .asSequence()
        .map {//map返回是Sequence<T>,故它屬於中間操做
            println("map: $it")
            return@map it + 1
        }
        .filter {//filter返回是Sequence<T>,故它屬於中間操做
            println("filter: $it")
            return@filter it % 2 == 0
        }
}
複製代碼

運行結果:優化

以上例子只有中間操做沒有末端操做,經過運行結果發現map、filter中並無輸出任何提示,這也就意味着map和filter的操做被延遲了,它們只有在獲取結果的時候(也便是末端操做被調用的時候)纔會輸出提示

  • 二、末端操做

序列的末端操做會執行原來中間操做的全部延遲計算,歡聚,一次末端操做返回的是一個結果,返回的結果能夠是集合、數字、或者從其餘對象集合變換獲得任意對象。上述例子加上末端操做:

fun main(args: Array<String>) {
    (0..6)
        .asSequence()
        .map {//map返回是Sequence<T>,故它屬於中間操做
            println("map: $it")
            return@map it + 1
        }
        .filter {//filter返回是Sequence<T>,故它屬於中間操做
            println("filter: $it")
            return@filter it % 2 == 0
        }
        .count {//count返回是Int,返回的是一個結果,故它屬於末端操做
            it < 6
        }
        .run {
            println("result is $this");
        }
}
複製代碼

運行結果

注意:判別是不是中間操做仍是末端操做很簡單,只須要看操做符API函數返回值的類型,若是返回的是一個Sequence<T>那麼這就是一箇中間操做,若是返回的是一個具體的結果類型,好比Int,Boolean,或者其餘任意對象,那麼它就是一個末端操做

三、怎麼建立序列(Sequences)?

建立序列(Sequences)的方法主要有:

  • 一、使用Iterable的擴展函數asSequence來建立。
//定義聲明
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
    return Sequence { this.iterator() }
}
//調用實現
list.asSequence()
複製代碼
  • 二、使用generateSequence函數生成一個序列。
//定義聲明
@kotlin.internal.LowPriorityInOverloadResolution
public fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> =
    if (seed == null)
        EmptySequence
    else
        GeneratorSequence({ seed }, nextFunction)

//調用實現,seed是序列的起始值,nextFunction迭代函數操做
val naturalNumbers = generateSequence(0) { it + 1 } //使用迭代器生成一個天然數序列
複製代碼
  • 三、使用序列(Sequence<T>)的擴展函數constrainOnce生成一次性使用的序列。
//定義聲明
public fun <T> Sequence<T>.constrainOnce(): Sequence<T> {
    // as? does not work in js
    //return this as? ConstrainedOnceSequence<T> ?: ConstrainedOnceSequence(this)
    return if (this is ConstrainedOnceSequence<T>) this else ConstrainedOnceSequence(this)
}
//調用實現
val naturalNumbers = generateSequence(0) { it + 1 }
val naturalNumbersOnce = naturalNumbers.constrainOnce()
複製代碼

注意:只能迭代一次,若是超出一次則會拋出IllegalStateException("This sequence can be consumed only once.")異常。

四、序列(Sequences)操做和集合操做性能對比

關於序列性能對比,主要在如下幾個場景下進行對比,經過性能對比你就清楚在什麼場景下該使用普通集合操做仍是序列操做。

  • 一、一樣數據操做在數據量級比較大狀況下。

使用Sequences序列

fun computeRunTime(action: (() -> Unit)?) {
    val startTime = System.currentTimeMillis()
    action?.invoke()
    println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}

fun main(args: Array<String>) = computeRunTime {
    (0..10000000)//10000000數據量級
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using sequence result is $this")
        }
}
複製代碼

運行結果:

不使用Sequences序列

fun computeRunTime(action: (() -> Unit)?) {
    val startTime = System.currentTimeMillis()
    action?.invoke()
    println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}

fun main(args: Array<String>) = computeRunTime {
    (0..10000000)//10000000數據量級
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using sequence result is $this")
        }
}
複製代碼

運行結果:

  • 二、一樣數據操做在數據量級比較小狀況下。

使用Sequences序列

fun computeRunTime(action: (() -> Unit)?) {
    val startTime = System.currentTimeMillis()
    action?.invoke()
    println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}

fun main(args: Array<String>) = computeRunTime {
    (0..1000)//1000數據量級
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using sequence result is $this")
        }
}
複製代碼

運行結果:

不使用Sequences序列

fun computeRunTime(action: (() -> Unit)?) {
    val startTime = System.currentTimeMillis()
    action?.invoke()
    println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}

fun main(args: Array<String>) = computeRunTime {
    (0..1000)//1000數據量級
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using list result is $this")
        }
}
複製代碼

運行結果:

經過以上性能對比發現,在數據量級比較大狀況下使用Sequences序列性能會比普通數據集合更優;可是在數據量級比較小狀況下使用Sequences序列性能反而會比普通數據集合更差。關於選擇序列仍是集合,記得前面翻譯了一篇國外的博客,裏面有詳細的闡述。博客地址

五、序列(Sequences)性能優化的原理

看到上面性能的對比,相信此刻的你火燒眉毛想要知道序列(Sequences)內部性能優化的原理吧,那麼咱們一塊兒來看下序列內部的原理。來個例子

fun main(args: Array<String>){
    (0..10)
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 6 }
        .run {
            println("by using sequence result is $this")
        }
}
複製代碼
  • 一、基本原理描述

序列操做: 基本原理是惰性求值,也就是說在進行中間操做的時候,是不會產生中間數據結果的,只有等到進行末端操做的時候纔會進行求值。也就是上述例子中0~10中的每一個數據元素都是先執行map操做,接着立刻執行filter操做。而後下一個元素也是先執行map操做,接着立刻執行filter操做。然而普通集合是全部元素都完執行map後的數據存起來,而後從存儲數據集中又全部的元素執行filter操做存起來的原理。

集合普通操做: 針對每一次操做都會產生新的中間結果,也就是上述例子中的map操做完後會把原始數據集循環遍歷一次獲得最新的數據集存放在新的集合中,而後進行filter操做,遍歷上一次map新集合中數據元素,最後獲得最新的數據集又存在一個新的集合中。

  • 二、原理圖解
//使用序列
fun main(args: Array<String>){
    (0..100)
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}
//使用普通集合
fun main(args: Array<String>){
    (0..100)
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}
複製代碼

經過以上的原理轉化圖,會發現使用序列會逐個元素進行操做,在進行末端操做find得到結果以前提前去除一些沒必要要的操做,以及find找到一個符合條件元素後,後續衆多元素操做均可以省去,從而達到優化的目的。而集合普通操做,不管是哪一個元素都得默認通過全部的操做。其實有些操做在得到結果以前是沒有必要執行的以及能夠在得到結果以前,就能感知該操做是否符合條件,若是不符合條件提早摒棄,避免沒必要要操做帶來性能的損失。

六、序列(Sequences)原理源碼徹底解析

//使用序列
fun main(args: Array<String>){
    (0..100)
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}
//使用普通集合
fun main(args: Array<String>){
    (0..100)
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}
複製代碼

經過decompile上述例子的源碼會發現,普通集合操做會針對每一個操做都會生成一個while循環,而且每次都會建立新的集合保存中間結果。而使用序列則不會,它們內部會不管進行多少中間操做都是共享同一個迭代器中的數據,想知道共享同一個迭代器中的數據的原理嗎?請接着看內部源碼實現。

使用集合普通操做反編譯源碼

public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      byte var1 = 0;
      Iterable $receiver$iv = (Iterable)(new IntRange(var1, 100));
      //建立新的集合存儲map後中間結果
      Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
      Iterator var4 = $receiver$iv.iterator();

      int it;
      //對應map操做符生成一個while循環
      while(var4.hasNext()) {
         it = ((IntIterator)var4).nextInt();
         Integer var11 = it + 1;
         //將map變換的元素加入到新集合中
         destination$iv$iv.add(var11);
      }

      $receiver$iv = (Iterable)((List)destination$iv$iv);
      //建立新的集合存儲filter後中間結果
      destination$iv$iv = (Collection)(new ArrayList());
      var4 = $receiver$iv.iterator();//拿到map後新集合中的迭代器
      //對應filter操做符生成一個while循環
      while(var4.hasNext()) {
         Object element$iv$iv = var4.next();
         int it = ((Number)element$iv$iv).intValue();
         if (it % 2 == 0) {
          //將filter過濾的元素加入到新集合中
            destination$iv$iv.add(element$iv$iv);
         }
      }

      $receiver$iv = (Iterable)((List)destination$iv$iv);
      Iterator var13 = $receiver$iv.iterator();//拿到filter後新集合中的迭代器
      
      //對應find操做符生成一個while循環,最後末端操做只須要遍歷filter後新集合中的迭代器,取出符合條件數據便可。
      while(var13.hasNext()) {
         Object var14 = var13.next();
         it = ((Number)var14).intValue();
         if (it > 3) {
            break;
         }
      }
   }
複製代碼

使用序列(Sequences)惰性操做反編譯源碼

  • 一、整個序列操做源碼
public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      byte var1 = 0;
      //利用Sequence擴展函數實現了fitler和map中間操做,最後返回一個Sequence對象。
      Sequence var7 = SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
      //取出通過中間操做產生的序列中的迭代器,能夠發現進行map、filter中間操做共享了同一個迭代器中數據,每次操做都會產生新的迭代器對象,可是數據是和原來傳入迭代器中數據共享,最後進行末端操做的時候只須要遍歷這個迭代器中符合條件元素便可。
      Iterator var3 = var7.iterator();
      //對應find操做符生成一個while循環,最後末端操做只須要遍歷filter後新集合中的迭代器,取出符合條件數據便可。
      while(var3.hasNext()) {
         Object var4 = var3.next();
         int it = ((Number)var4).intValue();
         if (it > 3) {
            break;
         }
      }

   }
複製代碼
  • 二、抽出其中這段關鍵code,繼續深刻:
SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
複製代碼
  • 三、把這段代碼轉化分解成三個部分:
//第一部分
val collectionSequence = CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100)))
//第二部分
val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)
//第三部分
val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)
複製代碼
  • 四、解釋第一部分代碼:

第一部分反編譯的源碼很簡單,主要是調用Iterable<T>中擴展函數將原始數據集轉換成Sequence<T>對象。

public fun <T> Iterable<T>.asSequence(): Sequence<T> {
    return Sequence { this.iterator() }//傳入外部Iterable<T>中的迭代器對象
}
複製代碼

更深刻一層:

@kotlin.internal.InlineOnly
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
    override fun iterator(): Iterator<T> = iterator()
}
複製代碼

經過外部傳入的集合中的迭代器方法返回迭代器對象,經過一個對象表達式實例化一個Sequence<T>,Sequence<T>是一個接口,內部有個iterator()抽象函數返回一個迭代器對象,而後把傳入迭代器對象做爲Sequence<T>內部的迭代器,也就是至關於給迭代器加了Sequence序列的外殼,核心迭代器仍是由外部傳入的迭代器對象,有點偷樑換柱的概念。

  • 五、解釋第二部分的代碼:

經過第一部分,成功將普通集合轉換成序列Sequence,而後如今進行map操做,實際上調用了Sequence<T>擴展函數map來實現的

val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)
複製代碼

進入map擴展函數:

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}
複製代碼

會發現內部會返回一個TransformingSequence對象,該對象構造器接收一個Sequence<T>類型對象,和一個transform的lambda表達式,最後返回一個Sequence<R>類型對象。咱們先暫時解析到這,後面會更加介紹。

  • 六、解釋第三部分的代碼:

經過第二部分,進行map操做後,而後返回的仍是Sequence對象,最後再把這個對象進行filter操做,filter也仍是Sequence的擴展函數,最後返回仍是一個Sequence對象。

val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)
複製代碼

進入filter擴展函數:

public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}
複製代碼

會發現內部會返回一個FilteringSequence對象,該對象構造器接收一個Sequence<T>類型對象,和一個predicate的lambda表達式,最後返回一個Sequence<T>類型對象。咱們先暫時解析到這,後面會更加介紹。

  • 七、Sequences源碼總體結構介紹

代碼結構圖: 圖中標註的都是一個個對應各個操做符類,它們都實現Sequence<T>接口

首先,Sequence<T>是一個接口,裏面只有一個抽象函數,一個返回迭代器對象的函數,能夠把它當作一個迭代器對象外殼。

public interface Sequence<out T> {
    /** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */
    public operator fun iterator(): Iterator<T>
}
複製代碼

Sequence核心類UML類圖

這裏只畫出了某幾個經常使用操做符的類圖

注意: 經過上面的UML類關係圖能夠獲得,共享同一個迭代器中的數據的原理實際上就是利用Java設計模式中的狀態模式(面向對象的多態原理)來實現的,首先經過Iterable<T>的iterator()返回的迭代器對象去實例化Sequence,而後外部調用不一樣的操做符,這些操做符對應着相應的擴展函數,擴展函數內部針對每一個不一樣操做返回實現Sequence接口的子類對象,而這些子類又根據不一樣操做的實現,更改了接口中iterator()抽象函數迭代器的實現,返回一個新的迭代器對象,可是迭代的數據則來源於原始迭代器中。

  • 八、接着上面TransformingSequence、FilteringSequence繼續解析.

經過以上對Sequences總體結構深刻分析,那麼接着TransformingSequence、FilteringSequence繼續解析就很是簡單了。咱們就以TransformingSequence爲例:

//實現了Sequence<R>接口,重寫了iterator()方法,重寫迭代器的實現
internal class TransformingSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
    override fun iterator(): Iterator<R> = object : Iterator<R> {//根據傳入的迭代器對象中的數據,加以操做變換後,構造出一個新的迭代器對象。
        val iterator = sequence.iterator()//取得傳入Sequence中的迭代器對象
        override fun next(): R {
            return transformer(iterator.next())//將原來的迭代器中數據元素作了transformer轉化傳入,共享同一個迭代器中的數據。
        }

        override fun hasNext(): Boolean {
            return iterator.hasNext()
        }
    }

    internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
        return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
    }
}
複製代碼
  • 九、源碼分析總結

序列內部的實現原理是採用狀態設計模式,根據不一樣的操做符的擴展函數,實例化對應的Sequence子類對象,每一個子類對象重寫了Sequence接口中的iterator()抽象方法,內部實現根據傳入的迭代器對象中的數據元素,加以變換、過濾、合併等操做,返回一個新的迭代器對象。這就能解釋爲何序列中工做原理是逐個元素執行不一樣的操做,而不是像普通集合全部元素先執行A操做,再全部元素執行B操做。這是由於序列內部始終維護着一個迭代器,當一個元素被迭代的時候,就須要依次執行A,B,C各個操做後,若是此時沒有末端操做,那麼值將會存儲在C的迭代器中,依次執行,等待原始集合中共享的數據被迭代完畢,或者不知足某些條件終止迭代,最後取出C迭代器中的數據便可。

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索