第十章 Scala 容器基礎(二十四):給集合建立一個懶加載視圖

Problem

    你正在使用一個巨大的集合,而且想建立一個懶加載的版本。只有在計算或者返回結果時才真正被調用。java

Solution

    除了Stream類,不論何時你建立一個Scala集合類的實例,你都建立了一個strict版本集合(任何操做都會被當即執行)。這意味着若是你新建了一個百萬元素的集合,這些元素會當即加載進內存。在Java中這是正常的,可是在Scala中你能夠選擇在集合上新建一個視圖。視圖可讓結果nonstrict,或者懶加載。這改變告終果集合,因此當調用集合的轉換方法的時候,只有真正要訪問集合元素的時候纔會執行計算,而且不像平時那樣是「當即執行」。(轉換方法是把一個輸入集合轉化爲一個輸出集合。)es6

    你能夠看下建立集合的時候使用view與不使用view的區別:算法

scala> val nums = 1 to 100
nums: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)

scala> val numsView = (1 to 100).view
numsView: scala.collection.SeqView[Int,scala.collection.immutable.IndexedSeq[Int]] = SeqView(...)

    不使用view建立一個Range就像你指望的結果同樣,一個100個元素的Range。然而,使用view的Range在REPL中出現了不一樣的輸出結果,一個叫作SeqView的東西。
數據庫

    這個SeqView帶有以下信息:
數組

  • 集合元素類型爲Int性能

  • 輸出結果scala.collection.immutable.IndexedSeq[Int],暗示了你使用force方法把view轉回正常集合時候你能獲得的集合元素類型。spa

    你會看到下面的信息,若是你強制把一個view轉回一個普通集合:scala

scala> val numsView = (1 to 100).view
numsView: scala.collection.SeqView[Int,scala.collection.immutable.IndexedSeq[Int]] = SeqView(...)

scala> val x = numsView.force
x: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)

    存在許多中方法能看到使用一個集合view的效果。首先,咱們來看一看foreach方法,它好像沒什麼區別。代理

scala> (1 to 100).foreach(x => print(x + " "))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 
scala> (1 to 100).view.foreach(x => print(x + " "))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

    這兩個例子都會直接打印出集合的100個元素,由於foreach方法並非一個轉換方法,因此對結果沒有影響。
code

    可是當你調用一個轉換方法的時候,你會戲劇性的發現結果變得不一樣了:

scala> (1 to 10).map(_ * 2)
res61: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

scala> (1 to 10).view.map(_ * 2)
res62: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...)

    結果不一樣了,應爲map是一個轉換方法。咱們來使用下面的代碼來更深層次的展現一下這種不一樣:

scala> (1 to 10).map{x => {
     |   Thread.sleep(1000)
     |   x * 2
     | }}
res68: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

scala> (1 to 10).view.map{x => {
     |   Thread.sleep(1000)
     |   x * 2
     | }}
res69: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...)

    不是用view的時候,程序會等待10秒,而後直接返回結果。使用view,程序則直接返回scala.collection.SeqView。

Discussion

    Scala文檔對view作出了一個說明:「僅僅對集合的結果構造了代理,它的元素構件只有一個要求...一個view是一個特殊類型的集合,它實現了集合的一些基本方法,可是對全部的transformers實現了懶加載

    一個transformer方法是可以從一個原有集合構造一個新的集合。這樣的方法包括map,filter,reverse等等。當你使用這些方法的時候,你就在把一個輸入集合轉化爲一個輸出集合。

    這就解釋了爲何foreach方法在使用view和沒有使用view時沒有任何區別:它不是一個transformer方法。可是map方法和其餘transformer方法好比reverse,就能夠有懶加載的效果:

scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)

scala> l.view.reverse
res70: scala.collection.SeqView[Int,List[Int]] = SeqViewR(...)
Use cases

    對於view,有兩個主要的使用場景:

  • 性能

  • 像處理數據庫視圖同樣處理集合

    關於性能,駕駛你遇到一種狀況,不得不處理一個十億元素的集合。若是你不得不作的話,你確定不但願直接在10億元素上運行一個算法,因此這時候使用一個視圖是有意義的。

    第二個應用場景讓你使用Scala view就像使用一個數據庫view同樣。下面這段代碼展現瞭如何把一個scala集合view看成一個數據庫view使用:

scala> val arr = (1 to 10).toArray
arr: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> val view = arr.view.slice(2, 5)
view: scala.collection.mutable.IndexedSeqView[Int,Array[Int]] = SeqViewS(...)

scala> arr(2) = 42

scala> view.foreach(println)
42
4
5

scala> view(0) = 10

scala> view(1) = 20

scala> view(2) = 30

scala> arr
res76: Array[Int] = Array(1, 2, 10, 20, 30, 6, 7, 8, 9, 10)

   改變數組中元素的值會改變view,改變view中對應數據元素的值一樣會改變數組元素值。當你想要修改一個集合子集的元素時,給集合建立一個view而後修改對應的元素是一個很是好的方法來實現這個目標。

    最後須要注意的是,不要錯誤的認爲使用view能夠節省內存。下面這兩個行爲會拋出一個「java.lang.OutOfMemoryError:Java heap space」錯誤信息:

scala> val a = Array.range(0,123456789)
java.lang.OutOfMemoryError: Java heap space

scala> val a = Array.range(0,123456789).view
java.lang.OutOfMemoryError: Java heap space

    最後說一句,視圖就是推遲執行,該用多大內存還使用多大內存,該遍歷多少元素仍是遍歷多少元素。說白了scala視圖就跟數據庫視圖同樣,不使用視圖就跟數據庫創建臨時表同樣。使用視圖,當原始集合改變的時候,不須要從新跑transformers方法,使用視圖則每次使用視圖的時候都會跑一次transformers方法內容。

scala> def compare(x: Int): Boolean = {
     |   println(s"compare $x and 5")
     |   return x < 5
     | }
compare: (x: Int)Boolean

scala> val l = List(1,2,3,4,5,6,7,8,9).view.filter(x => compare(x))
l: scala.collection.SeqView[Int,List[Int]] = SeqViewF(...)

scala> l.map(_ * 2)
res80: scala.collection.SeqView[Int,Seq[_]] = SeqViewFM(...)

scala> l.map(_ * 2).force
compare 1 and 5
compare 2 and 5
compare 3 and 5
compare 4 and 5
compare 5 and 5
compare 6 and 5
compare 7 and 5
compare 8 and 5
compare 9 and 5
res82: Seq[Int] = List(2, 4, 6, 8)
相關文章
相關標籤/搜索