1.序列:序列类型可以用来处理依次排列分组的数据。
列表:列表支持在头部快速添加和移除条目,不过并不提供快速的按下标访问的功能,因为实现这个功能需要线性地遍历列表。快速的头部添加和移除意味着模式匹配很顺畅。而列表的不可变性质帮助我们开发正确、高效的算法,因为我们不需要(为了防止意外)复制列表。
数组:数组允许我们保存一个序列的元素,并使用从零开始的下标高效地访问指定位置的元素值:
val fiveInts=new Array[Int](5)
println(fiveInts.toList)
//打印:List(0, 0, 0, 0, 0)
以下是如何初始化一个我们已知元素值的数组:
val fiveToOne=Array(5,4,3,2,1)
println(fiveToOne.toList)
//打印:List(5, 4, 3, 2, 1)
在Scala中以下标访问数组的方式是把下标放在圆括号里,而不是像Java一样放在方括号里:
fiveInts(0)=fiveToOne(4)
println(fiveInts.toList)
//打印:List(1, 0, 0, 0, 0)
列表缓冲:List类提供对列表头部的快速访问,对尾部访问没那么高效。一种避免reverse操作的可选方案是使用ListBuffer。ListBuffer是一个可变对象,帮助我们在需要追加元素来构建列表时可以更高效。ListBuffer提供了常量时间的往后追加和往前追加的操作。我们可以用+=操作符来往后追加元素,用+=:来往前追加元素。完成构建以后,我们可以调用ListBuffer的toList来获取最终的List:
import collection.mutable._
val buf=new ListBuffer[Int]()
buf+=1
buf+=2
3+=:buf
println(buf)
//打印:ListBuffer(3, 1, 2)
使用ListBuffer而不是List的另一个原因是防止可能出现的栈溢出。
数组缓冲:ArrayBuffer跟数组很像,除了可以额外地从序列头部或尾部添加或移除元素。所有的Array操作在ArrayBuffer都可用,不过由于实现的包装,会稍慢一点。新的添加和移除操作平均而言是常量时间的,不过偶尔会需要线性的时间,这是因为其实现需要不时地分配新的数组来保存缓冲的内容:
val buf=new ArrayBuffer[Int]()
buf+=12
buf+=15
3+=:buf
println(buf)
//打印:ArrayBuffer(3, 12, 15)
所有常规的数组操作都是可用的。
字符串(通过StringOps):我们需要了解的另一个序列是StringOps。它实现了很多序列方法。由于Predef有一个从String到StringOps的隐式转换,可以将任何字符串当作序列来处理:
val str="abcde"
println(str.mkString(","))
//打印:a,b,c,d,e
2.集和映射:当我们写下“Set”或“Map”时,默认得到的是一个不可变的对象。如果我们想要的是可变的版本,需要显示地做一次引入。这样的访问便利是通过Predef对象完成的,这个对象的内容在每个Scala源文件中都会隐式地引入:
object Predef{
type Map[A, +B] = collection.immutable.Map[A, B]
type Set[A] = collection.immutable.Set[A]
val Map = collection.immutable.Map
val Set = collection.immutable.Set
...
}
Predef利用“type”关键字定义了Set和Map这两个别名,分别对应不可变的集合不可变的映射的完整名称。名为Set和Map的val被初始化成指向不可变Set和Map的单例对象。因此Map等同于Predef.Map,而Predef.Map又等同于scala.collection.immutable.Map。这一点对于Map类型和对象都成立。如果想同时使用可变和不可变的集或映射,一种方式是引入包含可变版本的包:
import collection.mutable._
val mutaSet=mutable.Set(1,2,3)
println(mutaSet)
//打印:Set(1, 2, 3)
使用集:集的关键特征是它们会确保同一时刻,以==为标准,集里的每个对象都最多出现一次:
val words=mutable.Set.empty[String]
words+="run"
words+="spot"
println(words)
//Set(run, spot)
words+="run"
println(words)
//Set(run, spot)
默认的集合映射:对于大部分使用场景,由Set()、scala.collection.mutable.Map()等工厂方法提供的可变和不可变集和映射的实现通常都够了。scala.collection.mutable.Set()这个工厂方法返回一个scala.collection.mutable.HashSet,在内部使用哈希表:
println(mutable.Set(1,2).getClass)
//打印:class scala.collection.mutable.HashSet
对于不可变集和映射而言,情况要稍微复杂一些。举例来说,scala.collection.immutable.Set()工厂方法返回的类取决于我们传入了多少元素。对于少于五个元素的集,有专门的的特定大小的类与之对应,以此来达到最好的性能。一旦我们要求一个大于等于五个元素的集,这个工厂方法将返回一个使用哈希字典树的实现:
println(Predef.Set().getClass)
println(Predef.Set(1).getClass)
println(Predef.Set(1,2).getClass)
println(Predef.Set(1,2,3).getClass)
println(Predef.Set(1,2,3,4).getClass)
println(Predef.Set(1,2,3,4,5).getClass)
//class scala.collection.immutable.Set$EmptySet$
//class scala.collection.immutable.Set$Set1
//class scala.collection.immutable.Set$Set2
//class scala.collection.immutable.Set$Set3
//class scala.collection.immutable.Set$Set4
//class scala.collection.immutable.HashSet$HashTrieSet
跟集类似,对于少于五个元素的不可变映射,都会有一个特定的固定大小的映射与之对应,以此达到最佳性能。而一旦映射中的键值对个数达到或超过五个,则会使用不可变的HashMap。举例来说,如果添加一个元素到EmptySet,我们将得到一个Set1。如果添加一个元素到Set1,会得到一个Set2.如果这时再从Set2移除一个元素,我们又会得到另一个Set1.
排好序的集和映射:有时我们可能需要一个迭代器按照特定顺序返回元素的集或映射。对此,Scala集合类库提供了SortedSet和SortedMap特质。这些特质被TreeSet和TreeMap类实现,这些实现用红黑树来保持元素(对TreeSet而言)或键(对TreeMap而言)的顺序。具体顺序由Ordered特质决定,集的元素类型或映射的键的类型都必须混入或能够隐式转换成Ordered。TreeSet:
import collection.immutable._
var ts=TreeSet(new Rational(1,2),new Rational(2,3),new Rational(1,15))
println(ts)
//打印:TreeSet(1/15, 1/2, 2/3)
TreeMap:
var tm=collection.immutable.TreeMap(new Rational(1,2)->"1/2",new Rational(3,4)->"3/4")
println(tm)
//Map(1/2 -> 1/2, 3/4 -> 3/4)
tm+=(new Rational(1,15)->"1/15")
println(tm)
//Map(1/15 -> 1/15, 1/2 -> 1/2, 3/4 -> 3/4)
3.在可变和不可变集合类之间选择:除了可能更易于推敲外,在元素不多的情况下,不可变集合通常还可以比可变集合存储得更紧凑。为了让从不可变集转到可变集(或者反过来)更容易,Scala提供了一些语法糖。尽管不可变集和映射并不真正支持+=操作,Scala提供了一个变通的解读:
var tm=collection.immutable.TreeMap(new Rational(1,2)->"1/2",new Rational(3,4)->"3/4")
println(tm)
//Map(1/2 -> 1/2, 3/4 -> 3/4)
tm+=(new Rational(1,15)->"1/15")
println(tm)
//Map(1/15 -> 1/15, 1/2 -> 1/2, 3/4 -> 3/4)
通过将变量声明为var而不是val,那么这个映射就能够用+=操作来“更新”,尽管它是不可变的。首先,一个新的集合被创建出来,然后tm将被重新赋值指向新的映射。
4.初始化集合:除了用伴生对象的工厂方法,我们还可以更具体地定义集合的类型:
val stuff=collection.mutable.Set[Int]()
stuff++=List(1,2,3,4)
println(stuff)
//打印:Set(1, 2, 3, 4)
在可变和不可变集及映射间转换:首先用empty创建一个新类型的集合,然后用++或++=(视具体的目标集合而定)添加新元素:
val mutaSet=mutable.Set(5,3,4,2,1)
println(mutaSet)
//Set(1, 5, 2, 3, 4)
val imutaSet=immutable.Set.empty++mutaSet
println(imutaSet)
//Set(5, 1, 2, 3, 4)
5.元组:一个元组将一组固定个数的条目组合在一起,作为整体进行传递。不同于数组或列表,元组可以持有不同类型的对象。元组可以帮我们省去定义那些简单的主要承载数据的类的麻烦。如果只需要将一个整数和一个字符放在一起,我们需要的是一个元组,而不是List或Array:
val longest=("quick",1)
println(longest._1+"\t"+longest._2)
//打印:quick 1
不仅如此,还可以将元组的元素分别赋值给不同的变量:
val (word,idx)=longest
println(word+"\t"+idx)
//打印:quick 1
如果这里去掉圆括号:
val word,idx=longest
println(word)
//(quick,1)
println(idx)
//(quick,1)
需要注意的是,元组用起来太容易以至于我们可能会过度使用它们。如果这个组合有某种具体的含义,或者我们想给这个组合添加方法的时候,最好还是单独建一个类吧。举例来说,不建议用三元组来表示年、月、日的组合,建议Date类。这样意图更清晰,对读者更友好,也让编译器和语言有机会帮助我们发现程序错误。