第十章 Scala 容器基础(二十四):给集合创建一个懒加载视图

原创
2016/04/18 17:41
阅读数 473

Problem

    你正在使用一个巨大的集合,并且想创建一个懒加载的版本。只有在计算或者返回结果时才真正被调用。

Solution

    除了Stream类,不论什么时候你创建一个Scala集合类的实例,你都创建了一个strict版本集合(任何操作都会被立即执行)。这意味着如果你新建了一个百万元素的集合,这些元素会立即加载进内存。在Java中这是正常的,但是在Scala中你可以选择在集合上新建一个视图。视图可以让结果nonstrict,或者懒加载。这改变了结果集合,所以当调用集合的转换方法的时候,只有真正要访问集合元素的时候才会执行计算,并且不像平时那样是“立即执行”。(转换方法是把一个输入集合转化为一个输出集合。)

    你可以看下创建集合的时候使用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转回正常集合时候你能得到的集合元素类型。

    你会看到下面的信息,如果你强制把一个view转回一个普通集合:

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方法并不是一个转换方法,所以对结果没有影响。

    但是当你调用一个转换方法的时候,你会戏剧性的发现结果变得不同了:

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)

    



展开阅读全文
打赏
1
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
1
分享
返回顶部
顶部