scala里的List/Stream/View机制浅析
来源:互联网 发布:比基尼岛核试验 知乎 编辑:程序博客网 时间:2024/05/22 23:28
List机制浅析
scala里的List就是单向链表,一般通过下面方式来组装一个List:
val l = 1 :: 2 :: 3 :: Nil
Nil是空List,::是右结合操作符,所以上述写法相当于:
Nil.::(3).::(2).::(1)
我们来看看::连接符是如何实现的:
sealed abstract class List[+A] ..... { ... def isEmpty: Boolean def head: A def tail: List[A] def ::[B >: A](x: B): List[B] = new scala.collection.immutable.::(x, this) ..... }final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] { override def tail : List[B] = tl override def isEmpty: Boolean = false}
可见List是由Cons节点(即::类的实例)链接而成,每个Cons节点除了包含值(即head),还有一个指向尾List的tail指针。注意::类的类参数列表里val head前加override修饰符是因为::类重载了基类的head方法,在scala里成员方法和成员变量是被一视同仁的,也就是说,定义了一个成员方法后,我们不能再定义一个同名的成员变量。
另外,元素在加入List之前是要立即计算的,什么意思呢,像下面的语句:
val l = 1 :: {println("haha");2} :: {println("hehe");3} :: Nilprintln(l(0))
会输出:
haha
hehe
1
虽然我们只打印第1个元素,但第2、3个元素里的println动作也执行了,说明在List就绪之时,所有元素都必须计算出来。这样的话,List就无法去表示一个无限序列了。要表达一个无限序列,必须用Stream。
Stream机制浅析
Stream的写法是这样的:
val s = 1 #:: {println("haha");2} #:: {println("hehe");3} #:: Stream.emptyprintln(s)
输出:
Stream(1, ?)
说明Stream就绪时,仅有head元素是计算了的,其他元素均是未知。也就是说,元素在加入Stream之前是无需立即计算的,只在要用时才会计算,比如我们这样写:
println(s(1))
输出:
haha
2
那么,Stream是如何实现这种元素的惰性计算机制的呢?来看代码:
class ConsWrapper[A](tl: => Stream[A]) { /** Construct a stream consisting of a given first element followed by elements * from a lazily evaluated Stream. */ def #::(hd: A): Stream[A] = cons(hd, tl) ...... }object cons { /** A stream consisting of a given first element and remaining elements * @param hd The first element of the result stream * @param tl The remaining elements of the result stream */ def apply[A](hd: A, tl: => Stream[A]) = new Cons(hd, tl)}final class Cons[+A](hd: A, tl: => Stream[A]) extends Stream[A] { override def isEmpty = false override def head = hd @volatile private[this] var tlVal: Stream[A] = _ @volatile private[this] var tlGen = tl _ def tailDefined: Boolean = tlGen eq null override def tail: Stream[A] = { if (!tailDefined) synchronized { if (!tailDefined) { tlVal = tlGen() tlGen = null } } tlVal } }
我们发现,Stream跟List有点类似,也是一个单向链表,head指向元素值,但与List不同的是,tail指针并不指向尾队列,而是指向一个生成尾队列的函数:
tl: => Stream[A]
既然tail传递的是函数,而非尾队列,说明Stream除了首元素外,其他元素都不是立即计算的。
我们再看Cons类的tail方法,这个方法的实现有两个关键点:
1、尾队列的值tlVal是按需计算出来的,见tlVal = tlGen()这一句
2、一旦tlVal计算出来后,再次调用tail,就直接返回tlVal,不会再重复计算,这是通过tailDefined的检查来保证的。这说明,Stream具有记忆能力,可以缓存中间计算结果,以空间换时间。
所以,Stream适合用作无限序列的生成器,且可用于累积计算场景。
scala里的Stream与java8的Stream(流式操作)名字虽同,可却是全然不同的概念,scala里真正与java8的Stream对应的,其实是View。下面我们来分析View。
View机制浅析
我们看一个具体的例子:
@Test def testView: Unit ={ val l = 0 to 5 println(l.map(x => x * x).zip(10 to 15)) println(l.view.map(x => x * x).zip(10 to 15)) }
输出:
Vector((0,10), (1,11), (4,12), (9,13), (16,14), (25,15))
SeqViewMZ(…)
未调用view的操作序列输出一个Vector,而调用了view的操作序列仅输出一个SeqViewMZ对象(这里的M是Mapped,Z是Zipped的意思),并未真正计算,若要看到view的结果,需调force强制计算:
println(l.view.map(x => x * x).zip(10 to 15).force)
输出:
Vector((0,10), (1,11), (4,12), (9,13), (16,14), (25,15))
我们来看看view及map的实现:
override def view = new SeqView[A, Repr] { protected lazy val underlying = self.repr override def iterator = self.iterator override def length = self.length override def apply(idx: Int) = self.apply(idx) } override def map[B, That](f: A => B)(implicit bf: CanBuildFrom[This, B, That]): That = { newMapped(f).asInstanceOf[That] } protected def newMapped[B](f: A => B): Transformed[B] = new { val mapping = f } with AbstractTransformed[B] with Mapped[B] trait Mapped[B] extends Transformed[B] { protected[this] val mapping: A => B def foreach[U](f: B => U) { for (x <- self) f(mapping(x)) } }
view中underlying用lazy修饰,确保SeqView对应的真实容器是按需使用的。
map方法在SeqView基础上创建了一个MappedSeqView型实例,该实例所做的事情就是把x => x * x函子保存起来,即val mapping = f这句。MappedSeqView的foreach方法是要立即计算的,我们看到,它针对SeqView集合的每个元素都要先调一次x => x * x,接着才调foreach的f函子,如下面代码所示:
for (x <- self) f(mapping(x))
再来看zip调用:
override def zip[A1 >: A, B, That](that: GenIterable[B])(implicit bf: CanBuildFrom[This, (A1, B), That]): That = { newZipped(that).asInstanceOf[That]protected def newZipped[B](that: GenIterable[B]): Transformed[(A, B)] = new { val other = that } with AbstractTransformed[(A, B)] with Zipped[B] trait Zipped[B] extends Transformed[(A, B)] { protected[this] val other: GenIterable[B] def iterator: Iterator[(A, B)] = self.iterator zip other.iterator final override protected[this] def viewIdentifier = "Z" }
zip方法在MappedSeqView的基础上又创建了一个ZippedSeqView型实例,该实例将zip的对端序列(这里是10 to 15)缓存到other成员,即val other = that这句。ZippedSeqView的iterator方法则将上一个集合(即MappedSeqView)的迭代器与对端序列(即other成员)的迭代器做zip结合,如下面代码所示:
def iterator: Iterator[(A, B)] = self.iterator zip other.iterator
所以,view的调用过程像这样(foreach最终会转到iterator):
ZippedSeqView.foreach(
zip(other,MappedSeqView.foreach(
map(SeqView.foreach(
underlying.foreach)))))
等价于:
underlying.foreach(zip(other.item, map(underlying.item)))
也就是说,view是将操作累积起来了,它不像非view版本那样会生成临时集合。就我们这个例子而言,非view版本的处理过程是这样的:
Collection(0,1,2,3,4,5) -> Collection(0,1,4,9,16,25) -> Collection((0,10), (1,11), (4,12), (9,13), (16,14), (25,15))
而view版本则是这样的:
Collection(0,1,2,3,4,5) -> Collection((0*0,10), (1*1,11),(2*2,12),(3*3,13),(4*4,14),(5*5,15))
可见非view版本生成了额外的临时集合,且对原始集合(0,1,2,3,4,5)和临时集合(0,1,4,9,16,25)各做了一次遍历,最终生成结果集合((0,10), (1,11), (4,12), (9,13), (16,14), (25,15))。
而view版本由于不依赖临时集合,只需对原始集合(0,1,2,3,4,5)做一次遍历即可生成结果集合((0,10), (1,11), (4,12), (9,13), (16,14), (25,15))。这样的处理在原始集合数据量很大时,能有效节省内存、提升效率。
最后说明一下,view的这种处理方式有一个专有叫法:惰性化计算,什么意思呢?打个比方,就是在我们提交map计算给集合的时候,集合说:“知道了,我等会做”,其实它并没做,只有你真正需要结果时它才会不紧不慢的去执行,此谓之“惰性”。
对比
List与Stream:前者用于有限集合,后者用于无限集合。比如下面代码:
def constList(n:Int):List[Int] = n :: constList(n) def constStream(n:Int):Stream[Int] = n #:: constStream(n) @Test def testListStream: Unit ={ println(constStream(10)) println(constList(10)) }
println(constStream(10))会输出Stream(10, ?)
而println(constList(10))会栈溢出。
List和View:前者在做集合转换操作(如zip、map、flatMap等)时会生成中间集合,后者则不会,只在集合行为操作(如foreach)时一下子计算出中间累积操作的结果,后者在大集合时更省内存。
Stream和View:两者都会做惰性计算,但关注的维度不一样,前者是集合里元素计算的惰性化,后者则是集合本身计算的惰性化。事实上,Stream是有一个view方法的。
- scala里的List/Stream/View机制浅析
- scala学习笔记:理解stream和view
- Android View的事件分发机制浅析
- View的事件拦截机制浅析
- 怎么把Java的数组转成Scala里的List
- scala stream
- 有没有代码可以判断一个List<View>集合里的view是什么view
- 有没有代码可以判断一个List<View>集合里的view是什么view
- Scala的List
- scala的list操作
- 自定义View(二)--表层浅析View的事件分发机制和滑动冲突
- SF菜鸟笔记【JS里获取List View里的选中记录IDs】
- View的事件浅析
- Android View工作机制浅析(ppt)
- Symbian里的File,Stream,Store
- Scala浅析
- scala的array,list,tuple
- scala List集合的用法
- LaTex(PART VI) 摘要和关键字
- C++中strcmp的头文件问题
- 【bzoj 1191】超级英雄Hero(网络流)
- 【归并排序】【求逆序对】
- 关于kotlin语言在springMVC里面的应用---entity实体类
- scala里的List/Stream/View机制浅析
- Ubuntu 17.04 opencv源码编译安装
- 线性表及单链表
- freemark使用
- TFS映射时工作区存在报错的解决办法(一)
- python笔记
- SpringCloud第三篇-Feign
- Caffe::SetDevice 设置GPU的ID号
- B. The Eternal Immortality