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

極客熊貓發表於2018-08-31

簡述: 好久沒有釋出原創文章,一如既往,今天開始Kotlin淺談系列的第十講,一起來探索Kotlin中的序列。序列(Sequences)實際上是對應Java8中的Stream的翻版。從之前部落格可以瞭解到Kotlin定義了很多操作集合的API,沒錯這些函式照樣適用於序列(Sequences),而且序列操作在效能方面優於集合操作.而且通過之前函式式API的原始碼中可以看出它們會建立很多中間集合,每個操作符都會開闢記憶體來儲存中間資料集,然而這些在序列中就不需要。讓我們一起來看看這篇部落格內容:

  • 1、為什麼需要序列(Sequences)?
  • 2、什麼是序列(Sequences)?
  • 3、怎麼建立序列(Sequences)?
  • 4、序列(Sequences)操作和集合操作效能對比
  • 5、序列(Sequences)效能優化的原理
  • 6、序列(Sequences)原理原始碼完全解析

1、為什麼需要序列(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")
        }
}
複製程式碼

執行結果:

淺談Kotlin中的Sequences原始碼解析(十)
//轉化成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")
        }
}
複製程式碼

執行結果:

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

通過以上同一個功能實現,使用普通集合操作和轉化成序列後再做操作的執行時間差距不僅一點點,也就對應著兩種實現方式在資料集量級比較大的情況下,效能差異也是很大的。這樣應該知道為什麼我們需要使用Sequences序列了吧。

2、什麼是序列(Sequences)?

序列操作又被稱之為惰性集合操作,Sequences序列介面強大在於其操作的實現方式。序列中的元素求值都是惰性的,所以可以更加高效使用序列來對資料集中的元素進行鏈式操作(對映、過濾、變換等),而不需要像普通集合那樣,每進行一次資料操作,都必須要開闢新的記憶體來儲存中間結果,而實際上絕大多數的資料集合操作的需求關注點在於最後的結果而不是中間的過程,

序列是在Kotlin中運算元據集的另一種選擇,它和Java8中新增的Stream很像,在Java8中我們可以把一個資料集合轉換成Stream,然後再對Stream進行資料操作(對映、過濾、變換等),序列(Sequences)可以說是用於優化集合在一些特殊場景下的工具。但是它不是用來替代集合,準確來說它起到是一個互補的作用。

序列操作分為兩大類:

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

  • 1、中間操作

序列的中間操作始終都是惰性的,一次中間操作返回的都是一個序列(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
        }
}
複製程式碼

執行結果:

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

以上例子只有中間操作沒有末端操作,通過執行結果發現map、filter中並沒有輸出任何提示,這也就意味著map和filter的操作被延遲了,它們只有在獲取結果的時候(也即是末端操作被呼叫的時候)才會輸出提示

  • 2、末端操作

序列的末端操作會執行原來中間操作的所有延遲計算,歡聚,一次末端操作返回的是一個結果,返回的結果可以是集合、數字、或者從其他物件集合變換得到任意物件。上述例子加上末端操作:

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");
        }
}
複製程式碼

執行結果

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

注意:判別是否是中間操作還是末端操作很簡單,只需要看操作符API函式返回值的型別,如果返回的是一個Sequence<T>那麼這就是一箇中間操作,如果返回的是一個具體的結果型別,比如Int,Boolean,或者其他任意物件,那麼它就是一個末端操作

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

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

  • 1、使用Iterable的擴充套件函式asSequence來建立。
//定義宣告
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
    return Sequence { this.iterator() }
}
//呼叫實現
list.asSequence()
複製程式碼
  • 2、使用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 } //使用迭代器生成一個自然數序列
複製程式碼
  • 3、使用序列(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.")異常。

4、序列(Sequences)操作和集合操作效能對比

關於序列效能對比,主要在以下幾個場景下進行對比,通過效能對比你就清楚在什麼場景下該使用普通集合操作還是序列操作。

  • 1、同樣資料操作在資料量級比較大情況下。

使用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")
        }
}
複製程式碼

執行結果:

淺談Kotlin中的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資料量級
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using sequence result is $this")
        }
}
複製程式碼

執行結果:

淺談Kotlin中的Sequences原始碼解析(十)
  • 2、同樣資料操作在資料量級比較小情況下。

使用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")
        }
}
複製程式碼

執行結果:

淺談Kotlin中的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..1000)//1000資料量級
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .count { it < 100 }
        .run {
            println("by using list result is $this")
        }
}
複製程式碼

執行結果:

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

通過以上效能對比發現,在資料量級比較大情況下使用Sequences序列效能會比普通資料集合更優;但是在資料量級比較小情況下使用Sequences序列效能反而會比普通資料集合更差。關於選擇序列還是集合,記得前面翻譯了一篇國外的部落格,裡面有詳細的闡述。部落格地址

5、序列(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")
        }
}
複製程式碼
  • 1、基本原理描述

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

集合普通操作: 針對每一次操作都會產生新的中間結果,也就是上述例子中的map操作完後會把原始資料集迴圈遍歷一次得到最新的資料集存放在新的集合中,然後進行filter操作,遍歷上一次map新集合中資料元素,最後得到最新的資料集又存在一個新的集合中。

  • 2、原理圖解
//使用序列
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 }
}
複製程式碼

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

通過以上的原理轉化圖,會發現使用序列會逐個元素進行操作,在進行末端操作find獲得結果之前提早去除一些不必要的操作,以及find找到一個符合條件元素後,後續眾多元素操作都可以省去,從而達到優化的目的。而集合普通操作,無論是哪個元素都得預設經過所有的操作。其實有些操作在獲得結果之前是沒有必要執行的以及可以在獲得結果之前,就能感知該操作是否符合條件,如果不符合條件提前摒棄,避免不必要操作帶來效能的損失。

6、序列(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)惰性操作反編譯原始碼

  • 1、整個序列操作原始碼
 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;
         }
      }

   }
複製程式碼
  • 2、抽出其中這段關鍵code,繼續深入:
SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
複製程式碼
  • 3、把這段程式碼轉化分解成三個部分:
//第一部分
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)
複製程式碼
  • 4、解釋第一部分程式碼:

第一部分反編譯的原始碼很簡單,主要是呼叫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序列的外殼,核心迭代器還是由外部傳入的迭代器物件,有點偷樑換柱的概念。

  • 5、解釋第二部分的程式碼:

通過第一部分,成功將普通集合轉換成序列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>型別物件。我們先暫時解析到這,後面會更加介紹。

  • 6、解釋第三部分的程式碼:

通過第二部分,進行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>型別物件。我們先暫時解析到這,後面會更加介紹。

  • 7、Sequences原始碼整體結構介紹

程式碼結構圖: 圖中標註的都是一個個對應各個操作符類,它們都實現Sequence<T>介面

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

首先,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類圖

這裡只畫出了某幾個常用操作符的類圖

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

注意: 通過上面的UML類關係圖可以得到,共享同一個迭代器中的資料的原理實際上就是利用Java設計模式中的狀態模式(物件導向的多型原理)來實現的,首先通過Iterable<T>的iterator()返回的迭代器物件去例項化Sequence,然後外部呼叫不同的操作符,這些操作符對應著相應的擴充套件函式,擴充套件函式內部針對每個不同操作返回實現Sequence介面的子類物件,而這些子類又根據不同操作的實現,更改了介面中iterator()抽象函式迭代器的實現,返回一個新的迭代器物件,但是迭代的資料則來源於原始迭代器中。

  • 8、接著上面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)
    }
}
複製程式碼
  • 9、原始碼分析總結

序列內部的實現原理是採用狀態設計模式,根據不同的操作符的擴充套件函式,例項化對應的Sequence子類物件,每個子類物件重寫了Sequence介面中的iterator()抽象方法,內部實現根據傳入的迭代器物件中的資料元素,加以變換、過濾、合併等操作,返回一個新的迭代器物件。這就能解釋為什麼序列中工作原理是逐個元素執行不同的操作,而不是像普通集合所有元素先執行A操作,再所有元素執行B操作。這是因為序列內部始終維護著一個迭代器,當一個元素被迭代的時候,就需要依次執行A,B,C各個操作後,如果此時沒有末端操作,那麼值將會儲存在C的迭代器中,依次執行,等待原始集合中共享的資料被迭代完畢,或者不滿足某些條件終止迭代,最後取出C迭代器中的資料即可。

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

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

相關文章