1.List字面量:列表可能是Scala程序中最常使用的数据结构了:
val fruit=List("apples","oranges","pears")
val diag3=List(List(1,0,0),List(0,1,0),List(0,0,1))
val empty=List()
列表跟数组很像,不过有两个重要的区别。首先,列表是不可变的。也就是说,列表的元素不能通过赋值改变。其次,列表的结果是递归的(即链表),而数组是平的。
2.List类型:跟数组一样,列表也是同构的:同一个列表的所有元素都必须是相同的类型。元素类型为T的列表的类型写作List[T]:
val fruit: List[String] =List("apples","oranges","pears")
val diag3: List[List[Int]] =List(List(1,0,0),List(0,1,0),List(0,0,1))
val empty: List[Nothing] =List()
Scala的列表类是协变的。意思是对每一组类型S和T,如果S是T的子类型,那么List[S]就是List[T]的子类型。注意:空列表的类型为List[Nothing]。在Scala的类继承关系中,Nothing是底类型。由于列表是协变的,对于任何T而言,List[Nothing]都是List[T]的子类型。这也是为什么编译器允许我们编写如下的代码:
val xs: List[String] =List()
//List()也是List[String]类型的!
3.构建列表:所有的列表都构建自两个基础的构建单元:Nil和::(读作“cons”)。Nil表示空列表。中缀操作符::表示在列表前追加元素。也就是说,x::xs表示这样一个列表:第一个元素为x,接下来是列表xs的全部元素。因此,也可以这样来定义:
val fruit:List[String]=("apples"::("oranges"::("pears"::Nil)))
由于::以冒号结尾,::这个操作符是右结合的:A::B::C会被翻译成A::(B::C)。因此,我们可以在前面的定义中去掉圆括号。如:
val fruit:List[String]="apples"::"oranges"::"pears"::Nil
4.列表的基本操作:对列表的所有操作都可以用下面这三项来表述:
- head 返回列表的第一个元素
- tail 返回列表中除第一个元素之外的所有元素
- isEmpty 返回列表是否为空列表
head和tail方法只对非空列表有定义。用空列表调用会报:NoSuchElementException错误。
5.列表模式:列表也可以用模式匹配解开。列表模式可以逐一对应到列表表达式。我们既可以用List(...)这样的模式来匹配列表的所有元素,也可以用::操作符和Nil常量一点点地将列表解开。第一种模式:
val List(a,b,c)=fruit
println(a+"\t"+b+"\t"+c)
//打印:apples oranges pears
List(a,b,c)这个模式匹配长度为3的列表,并将三个元素分别绑定到模式变量a、b和c。如果我们事先并不知道列表中元素的个数,更好的做法是用::来匹配。如,匹配长度大于等于2:
val a::b::rest=fruit
println(a+"\t"+b+"\t"+rest)
//打印:apples oranges List(pears)
6.List类的初阶方法:如果一个方法不接收任何函数作为入参,就被称为初阶方法。
拼接两个列表:跟::操作相似的一个操作是拼接,写作:::。不同于::,:::接收两个列表参数作为操作元。xs:::ys的结果是一个包含了xs所有元素,加上ys所有元素的新列表。如:
println(List(1,2,3):::List(6,4,5))
//打印:List(1, 2, 3, 6, 4, 5)
跟cons类似,列表的拼接操作也是右结合的。如:
xs:::ys:::zs
会被解读成:
xs:::(ys:::zs)
分治原则:拼接(:::)是作为List类的一个方法实现。我们也可以通过对列表进行模式匹配来“手工”实现拼接。可以通过递归来实现:
def append[T](xs:List[T],ys:List[T]):List[T]=xs match {
case List()=>ys
case x::xs1=>append(xs1,x::ys)
}
println(append(List(1,2,3),List(6,4,5)))
//打印:List(3, 2, 1, 6, 4, 5)
这是一个尾递归,但xs的顺序被倒置了,如果不想被倒置,可以把x放到外面来:
case x::xs1=>x::append(xs1,ys)
但就没有尾递归优化了。
获取列表的长度:length:length方法计算列表的长度:
println(List(1,2,3).length)
//打印:3
不同于数组,在列表上的length操作相对更耗资源。找到一个列表的末尾需要遍历整个列表,因此需要消耗的资源与元素数量成正比的时间。这也是为什么将xs.isEmpty这样的测试换成xs.length==0并不是一个好主意。
访问列表的末端:last和init:我们已经知道基本的操作head和tail,它们分别获取列表的首个元素和除了首个元素剩余的部分。它们也分别有一个对偶方法:last返回(非空)列表的最后一个元素,而init返回除了最后一个元素之外剩余的部分:
println(fruit.last+"\t"+fruit.init)
//打印:pears List(apples, oranges)
不像head和tail那样在运行的时候消耗常量时间,init和last需要遍历整个列表来计算结果。因此它们的耗时跟列表的长度成正比,最好将数据组织成大多数访问都发生在列表头部而不是尾部。
反转列表:reverse:如果在算法中某个点需要频繁地访问列表的末尾,有时候先将列表反转,再对反转后的列表做操作是更好的做法:
println("abcde".toList.reverse)
//打印:List(e, d, c, b, a)
跟所有其他列表操作一样,reverse会创建一个新的列表,而不是对传入的列表做修改。由于列表是不可变的,这样的操作就算想做我们也做不了。
前缀和后缀:drop、take和splitAt:drop和take是对tail和init的一般化。它们返回的是列表任意长度的前缀或后缀。表达式“xs take n”返回列表xs的前n个元素。如果n大于xs.length,那么将返回整个xs列表。操作“xs drop n”返回列表xs除了前n个元素之外的所有元素。如果n大于等于xs.length,那么就返回空列表:
println(abcde take 2)
//List(a, b)
println(abcde drop 2)
//List(c, d, e)
splitAt操作将列表从指定的下标位置切开,返回这两个列表组成的对偶。它的定义来自如下这个等式:
xs splitAt n 等于 (xs tabke n,xs drop n)
如:
println(abcde splitAt 2)
//打印:(List(a, b),List(c, d, e))
不过,splitAt会避免遍历xs列表两次。
元素选择:apply和indices:apply方法支持从任意位置选取元素:
println(abcde apply 2)
//打印:c
可以简化为:
println(abcde(2))
//打印:c
对列表而言,从任意位置选取元素的操作之所以不那么常用,是因为xs(n)的耗时跟下标n成正比。事实上,apply是通过drop和head定义的:
xs apply n 等于 (xs drop n).head
Indices方法返回包含了指定列表所有有效下标的列表:
println(abcde.indices)
//打印:Range(0, 1, 2, 3, 4)
扁平化列表的列表:flatten:flatten方法接收一个列表的列表并将它扁平化,返回单个列表:
println(List(List(1,2),List(),List(3),List(4,5)).flatten)
//打印:List(1, 2, 3, 4, 5)
这个方法只能被应用于那些所有元素都是列表的列表。
将列表zip起来:zip和unzip:拉链(zip)操作接收两个列表,返回一个由对偶组成的列表:
println(abcde.indices zip abcde)
//打印:Vector((0,a), (1,b), (2,c), (3,d), (4,e))
如果两个列表的长度不同,那么任何没有配对上的元素将被丢弃:
println(abcde zip List(1,2,3))
//打印:List((a,1), (b,2), (c,3))
一个有用的特例是将列表和它的下标zip起来,最高效的做法是用zipWithIndex方法,这个方法会将列表中的每个元素和它出现在列表中的位置组合成对偶:
println(abcde.zipWithIndex)
//打印:List((a,0), (b,1), (c,2), (d,3), (e,4))
任何元组的列表也可以通过unzip方法转换回由列表组成的元组:
println(abcde.zipWithIndex.unzip)
//打印:(List(a, b, c, d, e),List(0, 1, 2, 3, 4))
显示列表:toString和mkString:toString操作返回列表的标准字符串表现形式:
println(abcde.toString())
//打印:List(a, b, c, d, e)
如果需要不同的表现形式,可以用mkString方法。xs mkString (pre,sep,post)涉及四个操作元:要显示的列表xs、出现在最前面的前缀字符串pre、在元素间显示的分隔字符串sep,以及出现在最后面的后缀字符串post:
println(abcde.mkString("[",",","]"))
//打印:[a,b,c,d,e]
mkString有两个重载的变种,让我们不必填写部分或全部入参:
//mkString源码
def mkString(start: String, sep: String, end: String): String =
addString(new StringBuilder(), start, sep, end).toString
def mkString(sep: String): String = mkString("", sep, "")
def mkString: String = mkString("")
源码显示最终调用的是addString方法,具体暂时不深究,有兴趣可以去看。
转换列表:iterator、toArray、copyToArray:为了在扁平的数组世界和递归的列表世界之间做数据转换,可以使用List类的toArray和Array类的toList方法:
val abcde="abcde".toList
println(abcde)
println(abcde.toArray)
//List(a, b, c, d, e)
//[C@2d8f65a4
还有一个copyToArray方法可以将列表中的元素依次复制到目标数组的指定位置:
xs copyToArray (arr,start)
将列表xs的所有元素复制至数组arr,从下标start开始。我们必须确保目标数组足够大,能够容纳整个列表:
val arr2=new Array[Int](10)
arr2.foreach(a=>print(a+" "))
println()
List(1,2,3).copyToArray(arr2,3)
arr2.foreach(a=>print(a+" "))
//0 0 0 0 0 0 0 0 0 0
//0 0 0 1 2 3 0 0 0 0
最后,如果要通过迭代器访问列表元素,可以用iterator方法:
val it=abcde.iterator
println(it.next())
println(it.next())
//a
//b
7.List类的高阶方法:许多对列表的操作都有相似的结构,有一些模式反复出现。在Java中,这些模式通常要通过固定的写法的for循环或while循环来组装。而Scala允许我们使用高阶操作符来更精简、更直接地表达,这些高阶操作是通过List类的方法实现的。
对列表作映射:map、flatMap和foreach:xs map f这个操作将类型为List[T]的列表xs和类型为T=>U的函数f作为操作元。它返回一个通过应用f到xs的每个元素后得到的列表:
println(List(1,2,3) map (_+1))
//打印:List(2,3,4)
flatMap操作符跟map类似,不过它要求右侧的操作元是一个返回元素列表的函数。它将这个函数应用到列表的每个元素,然后将所有结果拼接起来返回:
println(List("ads","fvb","fgh").flatMap(_.toList))
//List(a, d, s, f, v, b, f, g, h)
println(List("ads","fvb","fgh").map(_.toList))
//List(List(a, d, s), List(f, v, b), List(f, g, h))
我们可以看到,map返回的是列表的列表,而flatMap返回的是所有元素拼接起来的单个列表。flatMap很像map和flatten的结合体,在map结束后对列表使用flatten方法。
第三个映射类的操作是foreach。不同于map和flatMap,foreach要求有操作元是一个过程(结果类型为Unit的函数)。它只是简单地将过程应用到列表中的每个元素:
var sum=0
List(1,2,3,4) foreach(sum+=_)
println(sum)
//打印:10
过滤列表:filter、partition、find、takeWhile、dropWhile和span:“xs filter p”这个操作的两个操作元分别是类型为List[T]的xs和类型为T=>Boolean的前提条件函数p。这个操作将交出xs中所有p(x)为true的元素x:
println(List(1,2,3,4,5) filter(_%2==0))
//打印:List(2, 4)
partition方法跟filter很像不过返回的是一对列表。其中一个包含所有前提条件为true的元素,另一个包含所有前提条件为false的元素。它满足如下等式:
xs partition p 等于 (xs filter p,xs filter (!p(_)))
如:
println(List(1,2,3,4,5) partition (_%2==0))
//打印:(List(2, 4),List(1, 3, 5))
find方法跟filter也很像,不过它返回满足给定前提条件的第一个元素,而不是所有元素。如果xs中存在一个元素x满足p(x)为true,那么就返回Some(x)。如果对于所有元素而言p都为false,那么返回None:
println(List(1,2,3,4,5) find(_%2==0))
//Some(2)
println(List(1,2,3,4,5) find(_<0))
//None
takeWhile和dropWhile操作符也将一个前提条件作为右操作元。takeWhile操作返回列表中从开始连续满足p的最长前缀。dropWhile操作将去除列表连续满足p的最长前缀:
println(List(1,2,3,-4,5) takeWhile(_<3))
//List(1, 2)
println(List(1,2,3,-4,5) dropWhile(_<3))
//List(3, -4, 5)
span方法将takeWhile和dropWhile两个操作合二为一,就像splitAt将take和drop合二为一一样:
println(List(1,2,3,-4,5) span (_<3))
//打印:(List(1, 2),List(3, -4, 5))
对列表的前提条件检查:forall和exists:“xs forall p”这个操作接收一个列表xs和一个前提条件p作为入参。如果列表中所有元素都满足p就返回true。与此相反,“xs exists p”操作返回true的要求是xs中存在一个元素满足前提条件p:
println(List(1,2,3,-4,5) forall (_<3))
//false
println(List(1,2,3,-4,5) exists (_<3))
//true
折叠列表: /: 和 :\:左折叠操作“(z /:xs)(op)”涉及三个对象:起始值z、列表xs和二元操作op。折叠的结果是以z为前缀,对列表的元素依次连续应用op。例如:
(z /: List(a,b,c))(op) 等于 op(op(op(z,a),b),c)
合并两个列表:
val dig=List(1,2,3,4,5)
val d=(List[Int](0)/: dig )(_.::(_))
println(d)
//打印:List(5, 4, 3, 2, 1, 0)
/:操作符产生一棵往左靠的操作树。同理,:\这个操作符产生一棵往右靠的操作树:
(List(a,b,c) :\ z)(op) 等于 op(a,op(b,op(z,c)))
合并两个列表:
val dig=List(1,2,3,4,5)
val d=(dig:\List[Int](0) )((x,y)=>y.::(x))
println(d)
//打印:List(1, 2, 3, 4, 5, 0)
列表排序:sortWith:“xs sortWith before”这个操作对列表xs中的元素进行排序,其中“xs”是列表,而“before”是一个用来比较两个元素的函数:
println(List(1,-3,4,2,6) sortWith(_>_))
//打印:List(6, 4, 2, 1, -3)
8.List对象方法:到目前为止,所有的操作都是List类的方法。还有一些方法是定义在全局可访问对象scala.List上的,这是List类的伴生对象。
从元素创建列表:List.apply:List(1,2,3)跟List.apply(1,2,3)是等效的:
println(List.apply(1,2,3))
//打印:List(1, 2, 3)
创建数值区间:List.range:List.range(from,until),创建一个包含了从from开始递增到until减一的数的列表。终止值until并不是区间的一部分。range还有一个版本,接收step作为第三个参数。这个操作交出的列表元素是从from开始,间隔为step的值。step可以是正值,也可以是负值:
println(List.range(1,5))
println(List.range(1,9,2))
println(List.range(9,1,-2))
//打印:
//List(1, 2, 3, 4)
//List(1, 3, 5, 7)
//List(9, 7, 5, 3)
创建相同元素的列表:List.fill:fill方法创建包含零个或多个同一个元素拷贝的列表。它接收两个参数:要创建的列表长度和需要重复的元素。两个参数各自以不同的参数列表给出:
println(List.fill(5)('a'))
//List(a, a, a, a, a)
println(List.fill(2,3)('b'))
//List(List(b, b, b), List(b, b, b))
如果我们给fill的参数多于一个,那么它就会创建多维的列表。
表格化一个函数:List.tabulate:tabulate方法创建的是一个根据给定的函数计算的元素的列表。第一个参数列表给出要创建列表的维度,第二个参数列表传入一个函数:
println(List.tabulate(5)(n=>n*n))
//List(0, 1, 4, 9, 16)
println(List.tabulate(2,3)(_*_))
//List(List(0, 0, 0), List(0, 1, 2))
拼接多个列表:List.concat:concat方法将多个列表拼接在一起。要拼接的列表通过concat的直接入参给出:
println(List.concat(List(1),List(),List(2,3)))
//打印:List(1, 2, 3)
9.同时处理多个列表:元组的zipped方法将多个列表转化成一个元素为元组的列表:
println((List(1,2),List(4,5,6)).zipped.toList)
//打印:List((1,4), (2,5))
丢掉多于的元素。