Scala有一个非常通用,丰富,强大,可组合的集合库;集合是高阶的(high level)并暴露了一大套操作方法。很多集合的处理和转换可以被表达的简洁又可读,但不审慎地用它们的功能也会导致相反的结果。每个Scala程序员应该阅读 集合设计文档;通过它可以很好地洞察集合库,并了解设计动机。
1.数组(Array&ArrayBuffer)
1.1.Array
数组(Array)其实并不在scala.collection包里面,它属于scala包,直接对应于Java的数组,比如,Scala中的Array[Int], 在底层,与Java的int[]一样。
不过,Scala还是给Array提供了丰富得多的特性,比如支持泛型,比如支持类似于其他集合类型的遍历,转换等操作。 Arry对象还提供了大量的apply方法,这使得我们可以很方便的创建Array对象。下面,我们来看一些实际的例子。
//generic def printArray[T](arr: Array[T]) { println(arr.mkString("Array(", ",", ")")) } val nums = new Array[Int](3) for(i <- 0 until nums.length) nums(i) = i printArray(nums) //Array(0,1,2) val nums1 = Array(1,2,3) printArray(nums1) //Array(1,2,3) val nums2 = nums1.map( _ * 2) printArray(nums2) //Array(2,4,6) val nums3 = nums1.filter(_ % 2 == 0) printArray(nums3) //Array(2) val nums4 = for(n <- nums1 if n % 2 != 0) yield n * 2 printArray(nums4) //Array(2,6) println(nums4.sum) //8 val strs = Array("Beijing", "New York", "London") val strs1 = strs.reverse printArray( strs1) //Array(London,New York,Beijing)
一个真实的应用程序如下:
package com.tv189.foundation /** * Created by Administrator on 2015/7/30. */ object ArrayTest extends App{ def printArray[T](arr:Array[T]): Unit ={ println(arr.mkString("Array(",",",")")); } val nums=new Array[Int](3); for(i<-0 until nums.length){ nums(i)=i; } printArray(nums); //Array(0,1,2) val nums2=nums.map(_*2); printArray(nums2); //Array(0,2,4) val nums3=nums.filter(_%2==0); printArray(nums3); //Array(0,2) }
1.2.ArrayBuffer
Array在大多数语言中,都是不可变的,在Scala里也不例外。不过,Scala提供了一个可变的Array,这就是ArrayBuffer。 ArrayBuffer属于scala.collection.mutable包,使用时需要先引入。
通过ArrayBuffer,你可以很方便的添加或删除元素。很多情况下,我们在需要构建一个数组,预先不知道数组长度,但是构建完毕后,使用时不需要改变Array。 这时候,我们一般使用ArrayBuffer来构建数组,构建完毕后,调用它的toArray方法,得到不可变的Array供后续使用,以提高性能。
import scala.collection.mutable.ArrayBuffer val buffer = ArrayBuffer(1) println(buffer) buffer += 2 println(buffer) buffer += (3,4,5) println(buffer) buffer.prepend(0) println(buffer) buffer.trimEnd(2) println(buffer) buffer ++= Array(7,8) println(buffer) buffer.insert(4,4,5,6) println(buffer) val a = buffer.toArray println(a.getClass.getSimpleName)
NOTE
向ArrayBuffer的尾部添加或删除元素的效率很高,但是在别的位置插入元素的效率就比较低下了,因为这涉及到将该位置之后的元素后移。
因此,在使用ArrayBuffer时,请尽量在尾部进行操作。
2.List&ListBuffer
2.1.List
List可能是最常用的容器类了。而Scala为List提供了比其他语言丰富得多的操作,本节只介绍基本操作,一些复杂操作我们将在后续章节中慢慢接触到。
2.1.1.Nil
我们在Scala的类层级曾经提到过Nil对象,这是一个特殊的List对象,其类型是List[Nothing]。 你已经知道Nothing是所有类型的子类型,因此List[Nothing]是所有List类型的子类型,这意味着,Nil可用于任何需要List类型实例的场合。
Nil的另一个用处就是,在构建List时,用这个空元素作为起点。比如,语句 1 :: 2 :: Nil
能创建一个List,但是如果末尾没有Nil,只有1 :: 2
则由于Int类没有::
这个操作,在语法上就会出错了。
2.1.2.构建List
Scala中构建List的方式可谓灵活多变。最常用的是apply方法和使用操作符(::)这两种,还有比较少见的fill方法,也可用于创建List。
-
使用apply方法
前面我们介绍过apply方法,List对象提供了一整套这样的方法供构建List用,比如List(1,2,3)
。
-
使用
::
操作符
::
操作符是List特有的方法,属于右结合操作符,适合于List这样在头部操作效率较高的数据结构。比如,1 :: 2 :: 3 :: Nil
, 它的计算步骤是(1 :: (2 :: (3 :: Nil)))
,首先将3添加到空列表中,然后是2,最后才是1。
-
使用range和fill方法
除了apply方法外,List对象还提供了几个实用方法来创建List,range和fill就是其中的两个。
range方法用于构建一个从开始到结束的一系列数字,包含下边界,但是不包含上边界。另外,range是泛型的,接受类型参数,但是对类型有约束,必须是整数类型range[T: Integral]
。
fill方法则用于用一个初始元素创建指定长度的List,该List的每一个元素值都一样。
下面是创建List的示例。
val nums1 = List(1,2,3) val strs1 = List("Beijing", "Shanghai", "Guangzhou") println(nums1) println(strs1) val nums2 = 1 :: 2 :: 3 :: Nil val strs2 = "Beijing" :: "Shanghai" :: "Guangzhou" :: Nil println(nums2) println(strs2) val nums3 = List.range[Int](1,6,2) //from 1 to 6, with step 2 val chars = List.range[Char](‘a‘,‘c‘) println(nums3) println(chars) val fill = List.fill(3)("original") println(fill)
2.1.3.常用的操作
List继承了通用的集合特质,比如SeqLike,IterableLike等等,常用的在集合上能进行的操作,比如contains, filter, sortWith等等, 此外List本身还提供了许多方法,因此,可以进行的操作非常多,以下选取常用的几个来介绍他们的用法。
- isEmpty: Boolean:判断List是否有元素
- length: Int:获取List的长度
- head: A:取List的第一个元素
- tail: List[A]:获得List除了第一个元素之外,剩下的list
- last: A:获取List的最后一个元素
- init: List[A]:获取除了最后一个元素之外的list
- apply(n): A:获取第n个元素
- drop(n): List[A]:去掉左边n个元素,返回剩下的list,对应的,有dropRight(n)方法
- take(n): List[A]:获取左边n个元素组成的list,也有对应的takeRight(n)方法
- dropWhile(p: A => Boolean): List[A]:去掉左边满足条件的元素
- takeWhile(p: A => Boolean): List[A]:获取左边满足条件的元素
- contains(elem: Any): Boolean:重载SeqLike的方法,判断List是否包含某元素
- exists(p: A => Boolean): Boolean:重载IterableLike的方法,判断List中是否包含满足给定条件的元素
- forall(p: A => Boolean): Boolean:重载IterableLike的方法,判断是否List中所有的元素都满足给定条件
- filter(p: A => Boolean): List[A]:是TraversableOnce特质提供的方法,挑选复合条件的元素组成新的List
- mkString:是TraversableOnce特质提供的方法,有多个重载,可方便的将一个序列转换为String
- splitAt(n: Int): (List[A], List[A]):将List从给定位置分成两个部分,返回一对List。
- :::[B >: A](prefix: List[B]): List[B]:将给定List中的所有元素添加到本List之前。这个方法是右结合运算。
println(Nil.isEmpty) //true val nums = List(1, 2, 3, 4, 5) println(nums.length) //5 println(nums.head) //1 println(nums.tail) //List(2, 3, 4, 5) println(nums.last) //5 println(nums.init) //List(1, 2, 3, 4) println(nums.reverse) //List(5, 4, 3, 2, 1) println(nums(3)) //4 println(nums.drop(2)) //List(3, 4, 5) println(nums.take(2)) //List(1, 2) println(nums.takeWhile(_ % 2 != 0 )) //List(1) println(nums.dropWhile(_ % 2 != 0)) ////List(2, 3, 4, 5) println(nums.exists(_ == 3)) //true println(nums.forall(_ % 2 ==0)) //false println(nums.filter(_ % 2 ==0)) //List(2, 4) println(nums.mkString("Numbers[", ",", "]")) //Numbers[1,2,3,4,5] println(nums.splitAt(3)) //(List(1, 2, 3),List(4, 5)) println(nums.sortWith((x,y) => x > y)) val nums2 = List(6,7) println(nums ::: nums2) //List(1, 2, 3, 4, 5, 6, 7)
请注意
在List的头部进行操作,比尾部效率要高,这是因为List是由单链表实现,访问第一个元素只需要O(1)的时间,而最后一个元素则需要O(n)。
因此使用List时,请尽量设计为越常访问的数据在越靠前,构建List时,尽量从头部添加元素。
2.2.ListBuffer
List是不可变的,在头部操作效率比较高。如果你需要频繁的更改元素,而且在尾部的操作比较多,可以使用对应的可变集合,ListBuffer。 在ListBuffer上prepend(+=)和append(+=:)操作都是常数时间O(1)。与ArrayBuffer相似,在构建完毕之后,可以使用toList得到不可变的List。
import scala.collection.mutable.ListBuffer val buffer = new ListBuffer[Int] buffer += 2 buffer ++= List(3,4) buffer.+=:(1) // equals to ‘1 +=: buffer‘ val list = buffer.toList println(list)
请注意+=:
是右结合操作符(所有以:
结尾的操作符都是右结合的),因此,不能写作buffer +=: 1
,如果想写成操作符形式,需要将buffer放在右边,
也就是1 +=: buffer
3.Set
[Scala Level: A1]
Set最大的特点就是不能有重复的元素,所有的元素都是唯一的。判断元素是否唯一,是通过==
方法,由于这个方法定义在根类Any上,所以能对所有的对象调用该方法。 不过,设计类时,如果有特有的相等逻辑,需要重载==
以确保你的类有正确的等价关系。
Set分为可变Set和不可变的两个版本,处于不同的package内,这一点与Array和List不同。
3.1.不可变的Set
不可变Set位于scala.collection.immutable包里,由于Predef里包含了不可变Set的定义,默认情况下,你使用的Set是不可变Set。 Set常常用于要求元素唯一的场景,比如key-value中的keySet。
与List一样,Scala也给Set提供了很多的操作,常用的在集合上的操作都可以用于Set。
3.2.可变Set
要使用可变Set,需要显示引用它,如下面例子所示。Scala为可变Set提供了一系列操作,可以方便的在Set中添加或删除元素。
val set = Set(1,2,3) println(set.getClass.getName) //scala.collection.immutable.Set println(set.exists(_ % 2 == 0)) //true println(set.drop(1)) //Set(2,3) import scala.collection.mutable.Set //import can be anywhere val mutableSet = Set(1,2,3) println(mutableSet.getClass.getName) //scala.collection.mutable.HashSet mutableSet.add(4) mutableSet.remove(1) mutableSet += 5 mutableSet -= 2 println(mutableSet) //Set(5, 3, 4) val another = mutableSet.toSet println(another.getClass.getName) //scala.collection.immutable.Set
请注意
虽然可变Set和不可变Set都有添加或删除元素的操作,但是有一个非常大的差别。对不可变Set进行操作,会产生一个新的set,原来的set并没有改变,这与List一样。
而对可变Set进行操作,改变的是该Set本身,与ListBuffer类似。
3.3.SortedSet(TreeSet)
默认情况下,我们用的Set都是以HashSet实现,也就是底层数据结构是哈希表。我们知道,哈希表的特点是能快速定位元素,因此HashSet的添加,删除等操作都是常数时间。 但是哈希表没有确定的排序,因此,在需要排序的情况下,可以使用SortedSet。
SortedSet实际上是一个特质(trait),它有一个实现类TreeSet,由红黑树实现。你可以选择用SortedSet或者直接使用TreeSet, 另外,他们也有可变可不可变版本,可根据需要使用。
import scala.collection.immutable.SortedSet val set = SortedSet(2, 6, 7, 3, 5, 4, 1, 8) println(set.getClass.getName) //scala.collection.immutable.TreeSet println(set) //TreeSet(1, 2, 3, 4, 5, 6, 7, 8) import scala.collection.mutable.TreeSet val mSet = TreeSet(3,9) mSet += 5 mSet += 1 println(mSet) //TreeSet(1, 3, 5, 9)
4.Map
Map是一系列键值对(key-value pair)的集合,它将键(key)和值(value)对应起来,可以通过键快速定位值。与其他集合类一样,Scala提供了很多方便实用的方法来操作Map。 Map与Set类似,按可变性,可分为可变Map和不可变Map,按照内部实现,可分为HashMap和TreeMap。
4.1.构建Map
构建不可变的Map可使用Map对象的apply方法,该方法接受一系列键值对作为参数。比如:
val scores = Map(("Jackie", 90), ("Abby", 93), ("Tim", 87)) val caps = Map("China" -> "Beijing", "France" -> "Paris", "Norway" -> "Oslo")
以上构建的Map是默认的不可变Map。也可以创建可变的Map,然后往里添加数据,比如:
val teachers = scala.collection.mutable.Map("Chunni" -> "History") teachers += "Jim" -> "Math" teachers("Zhou") = "English" println(teachers)
同其他可变集合一样,可变Map也可以通过toMap
方法返回一个不可变Map。
请注意
update方法,与apply方法一样,比较特殊,有简写方式,比如,obj.update(k, v),可以写为obj(k) = v。
因此,以上teachers("Zhou") = "English"
等价于teachers.update("Zhou", "English")
。
4.2.访问Map元素
访问Map元素最直接的莫过于apply方法,map(k)即可获得以k为键的值,如
scores("Abby")
使用apply方法时,如果指定的键不存在,会抛出异常,为方便起见,可使用get方法get(key: A): Option[B]
, 这个方法返回一个Option,你可以在使用时检查该Option是否为空。
val scores = Map(("Jackie", 90), ("Abby", 93), ("Tim", 87)) val caps = Map("China" -> "Beijing", "France" -> "Paris", "Norway" -> "Oslo") //scores("no") //will throw an exception, key not found val score = scores.get("no") //None if(score.isEmpty) { //handle the empty case } println(scores.getOrElse("no", 90))
如果你只在为空的情况下设置一个默认值,而不做其他处理,也可以使用getOrElse方法,该方法的第二个参数是默认值。如println(scores.getOrElse("no", 90))
.
请注意
Scala中,‘[]‘一般是用于在泛型中指定类型。而‘()‘用于方法调用。访问Map的元素,与Array一样,是用‘()‘调用apply方法。
这与C#或Java经常用‘[]‘访问Array或Map的数据不同。
4.3.Map的遍历
要遍历一个Map,可以通过它的keySet,也可以直接遍历它的键值对。如下:
val capitals = Map("China" -> "Beijing", "France" -> "Paris", "Norway" -> "Oslo") println("---use key iterator---") val itr = capitals.keysIterator while (itr.hasNext) println(itr.next()) println("---use for on key set---") for(k <- capitals.keySet) println(capitals(k)) println("---use for on key value pair---") for((k, v) <- capitals) println("The capital of %s is %s".format(k, v))
4.4.SoretedMap(TreeMap)
与Set一样,默认情况下,使用的是HashMap。如果需要排序的话,可以使用SortedMap,或直接用TreeMap类。 为什么与Set一样?因为Map的实现方式就在于其Key Set的实现,HashMap就是指其Key Set是HashSet实现,而TreeMap的Key Set为TreeSet实现。
import scala.collection.immutable.TreeMap var capitals = TreeMap("China" -> "Beijing", "Norway" -> "Oslo", "France" -> "Paris") capitals += "Austria" -> "Vienna" println(capitals) //sorted import scala.collection.mutable.LinkedHashMap val mutable = LinkedHashMap("China" -> "Beijing", "Norway" -> "Oslo", "France" -> "Paris") mutable += "Austria" -> "Vienna" println(mutable) //insertion order
请注意 Scala没有提供可变的TreeMap,如果需要的话,可以使用Java的TreeMap。不过,如果只需按插入顺序排序的话,可使用LinkedHashMap。
5.Tuple
[Scala Level: A1]
Tuple是一个方便的集合,可以容纳不同类型的元素,因此它最常用的场景就是用于返回多个对象,而这对象之间值逻辑关系并不紧密,不够格为他们设计一个类。
5.1.创建Tuple
当你把一系列对象用小括号‘()‘括起来时,那就是一个Tuple了。当然,由于Tuple其实是scala里面定义好的22个类,Tuple1...Tuple22, 你也可以直接用这些类。
val a = (1, "a") val b = Tuple2(1,"a")
以上两个语句是等价的,都是创建了一个Tuple2[Int, String]的对象。从这里我们也能看到,Tuple是静态类型的,其类型由编译器通过类型推导(type inference)得知。
5.2.使用Tuple
使用Tuple里的元素,只需用_i
按位置获取,比如a._1
。请看下面的实例。
val score = ("Jimmy", 90, 92 ) def total(a: Int, b: Int) = a + b println("%s got %d in total".format(score._1, total(score._2, score._3)))
请注意
Tuple的位置从1开始,而不是像数组或其他集合那样,从0开始。
Tuple最常用的就是可一次返回多个对象,而且,还可以用一个语句将这多个对象赋值给其他变量,如下例所示。
def findHighest(scores: Map[String,Int]) = { var max = 0 var name = "" for((k, v) <- scores) { if(v > max) { max = v name = k } } (name, max) } val scores = Map("Jimmy" -> 90, "Abby" -> 92, "Ella" -> 88) val (champion, score) = findHighest(scores) println("The champion: %s, score: %d".format(champion, score))
以上findHighest需要返回两个值,为这两个值新建一个类在此处仿佛不很必要,因此,用Tuple将他们打包返回给调用者。 另外,在使用的时候,可用Tuple一次给多个变量赋值,如上面的val (champion, score) = findHighest(scores)
。
请注意
如果返回的多个对象之间有紧密的逻辑关系,或者经常使用,可能定义一个类会是更好的选择。总之,让程序简洁又容易理解是最终的目标。
参考文献:
《Effective Scala》
http://alvinalexander.com/scala/simple-scala-akka-actor-examples-hello-world-actors