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} :: Nil
println(l(0))
会输出:
haha
hehe
1
虽然我们只打印第1个元素,但第2、3个元素里的println动作也执行了,说明在List就绪之时,所有元素都必须计算出来。这样的话,List就无法去表示一个无限序列了。要表达一个无限序列,必须用Stream。
Stream机制浅析
Stream的写法是这样的:
val s = 1 #:: {println("haha");2} #:: {println("hehe");3} #:: Stream.empty
println(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方法的。