快学Scala第13章----集合

本章要点

  • 所有集合都扩展自Iterable特质
  • 集合有三大类:序列、集、映射
  • 对于几乎所有集合类,Scala都同时提供了可变的和不可变的版本
  • Scala列表要么是空的,要么拥有一头一尾,其中尾部本身又是一个列表
  • 集是无先后次序的集合
  • 用LinkedhashSet 来保留插入顺序,或者用SortedSet来按顺序进行迭代
  • ‘+’ 将元素添加到无先后次序的集合中; +: 和 :+ 向前或向后追加到序列; ++将两个集合串接在一起; -和–移除元素
  • Iterable和Seq特质有数十个用于常见操作的方法
  • 映射、折叠和拉链操作是很有用的技巧,用来将函数或操作应用到集合中的元素

主要的集合特质

Scala集合继承层级中的关键特质:

Iterable值的是那些能申城用来访问集合中所有元素的Iterator的集合,类似于C++的迭代器。

val coll = ...  //某种Iterable
val iter = coll.iterator
while (iter.hasNext) {
  对iter.next() 执行某种操作
}

Seq是一个有先后次序的值的序列,例如数字或列表。IndexedSeq允许我们通过使用下标的方式快速访问元素。

Set是一组没有先后次序的值的集合。在SortedSet中,元素是排序的。

Map是一组(key, value)对偶。 SortedMap按照键的排序的。

每个Scala集合特质或类都有一个带有apply方法的伴生对象,这个apply方法可以用来构建该集合的实例,而不用使用new,这样的设计叫做”统一创建原则”。


可变和不可变集合

Scala同时支持可变和不可变的集合。Scala优先采用不可变集合,因此你可以安全的共享其引用。任何对不可变集合的修改操作都返回的是一个新的不可变集合,它们共享大部分元素。

def digits(n: Int): Set[Int] = {
  if (n < 0) digits(-n)
  else if (n < 10) Set(n)
  else digits(n / 10) + (n % 10)
}

这个例子利用递归不断的产生新的集合,但是要注意递归的深度。


序列

最重要的不可变序列:

Vector是ArrayBuffer的不可变版本,和C++的Vector一样,可以通过下标快速的随机访问。而Scala的Vector是以树形结构的形式实现的,每个节点可以有不超过32个子节点。这样对于有100万的元素的向量而言,只需要4层节点。

Range表示一个整数序列,例如0,1,2,3,4,5 或 10,20,30 . Rang对象并不存储所有值而只是起始值、结束值和增值。

最有用可变序列:


列表

在Scala中,列表要么是Nil(空列表),要么是一个head元素加上一个tail,而tail又是一个列表。例如:

val digits = List(4,2)
digits.head  // 4
digits.tail   // List(2)
digits.tail.head // 2
digits.tail.tail  // Nil

:: 操作符从给定的头和尾创建列表:

9 :: List(4, 2)  // List(9,4,2)
// 等同于
9 :: 4 :: 2 ::  Nil   // 这里是又结合的

遍历链表:可以使用迭代器、递归或者模式匹配

def sum(lst: List[Int]): Int = {
  if (lst == Nil) 0 else lst.head + sum(lst.tail)
}

def sum(lst: List[Int]): Int = lst match {
  case Nil => 0
  case h :: t => h + sum(t)
}

可变列表

可变的LinkedList既可以修改头部(对elem引用赋值),也可以修改尾部(对next引用赋值):

val lst = scala.collection.mutable.LinkedList(1, -2, 7, -9)
// 修改值
var cur = lst
while (cur != Nil) {
  if (cur.elem < 0) cur.elem = 0
  cur = cur.next
}

// 去除每两个中的一个
var cur = lst
while (cur != Nil && cur.next != Nil) {
  cur.next = cur.next.next
  cur = cur.next
}

注意: 如果你想要将列表中的某个节点变成列表的最后一个节点,你不能够将next引用设为Nil,而应该将next引用设为LinkedList.empty。也不要设为null,不然在遍历该链表时会遇到空指针错误。


集是不重复的元素的集合,与C++中的set相同。集并不保留元素插入的顺序,默认情况下,集以哈希集实现。

而链式哈希集可以记住元素插入的顺序,它会维护一个链表来达到这个目的。

val weekdays = scala.collection.mutable.LinkedHashSet("Mo", "Tu", "We", "Th", "Fr")

对于SortedSet已排序的集使用红黑树实现的。Scala没有可变的已排序的集,前面已经讲过。

集的一些常见操作:

val digits = Set(1,7,2,9)
digits contains 0   // false
Set(1, 2) subsetOf digits  // true

val primes = Set(2,3,5,7)
digits union primes  // Set(1,2,3,5,7,9)
// 等同于
digits | primes  // 或 digits ++ primes

digits intersect primes  // Set(2, 7)
// 等同于
digits & primes

digits diff primes  // Set(1, 9)
// 等同于
digits -- primes

用于添加或去除元素的操作符

一般而言, + 用于将元素添加到无先后次序的集合,而+: 和 :+ 则是将元素添加到有先后次序的集合的开头或是结尾。

Vector(1,2,3) :+ 5  // Vector(1,2,3,5)
1 +: Vector(1,2,3)  // Vector(1,1,2,3)

常用方法

Iterable特质最重要的方法:

Seq特质在Iterable特质的基础上又增加的一些方法:


将函数映射到集合

map方法可以将某个函数应用到集合的每一个元素并产出其结果的集合。例如:

val names = List("Peter", "Paul", "Mary")
names.map(_.toUpperCase)   // List("PETER", "PAUL", "MARY")
// 等同于
for (n <- names) yield n.toUpperCase

如果函数产出一个集合而不是单个值得话,则使用flatMap将所有的值串接在一起。例如:

def ulcase(s: String) = Vector(s.toUpperCase(), s.toLowerCase())
names.map(ulcase) // List(Vector("PETER", "peter"), Vector("PAUL", "paul"), Vector("MARY", "mary"))
names.flatmap(ulcase)  // List("PETER", "peter", "PAUL", "paul", "MARY", "mary")

collect方法用于偏函数—并没有对所有可能的输入值进行定义的函数。例如:

"-3+4".collect {case ‘+‘ => 1; case ‘-‘ => -1}    // Vector(-1,1)
"-3+4".collect {case ‘-‘ => -1}    // Vector(-1)
"-3+4".collect {case ‘+‘ => 1}    // Vector(1)
"-3+4".collect {case ‘*‘ => 1}    // Vector()

foreach方法将函数应用到各个元素但不关心函数的返回值。

names.foreach(println)

化简、折叠和扫描

reduceLeft、reduceRight、foldLeft、foldRight、scanLeft、scanRight方法将会用二元函数来组合集合中的元素:

List(1,7,2,9).reduceLeft(_ - _)  // ((1 - 7) - 2) - 9
List(1,7,2,9).reduceRight(_ - _)  // 1 - (7 - (2 - 9))

List(1,7,2,9).foldLeft(0)(_ - _)   // 0 - 1 -7 - 2 - 9
List(1,7,2,9).foldRight(0)(_ - _)  // 1 - (7 - (2 - (9 - 0)))

(1 to 10).scanLeft(0)(_ + _)  // Vector(0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55)

拉链操作

前面的章节已经讲过拉链操作。除了zip方法外,还有zipAll和zipWithIndex方法

List(5.0, 20,0, 9.7, 3.1, 4.3).zipAll(List(10, 2), 0.0, 1)  // List((5.0, 10), (20.0, 2), (9.7, 1), (3.1, 1), (4.3, 1))

"Scala".zipWithIndex   // Vector((‘S‘, 0), (‘c‘, 1), (‘a‘, 2), (‘l‘, 3), (‘a‘, 4) )

迭代器

迭代器的好处就是你不用将开销很大的集合全部读进内存。例如读取文件操作,Source.fromFile产出一个迭代器,使用hasNext和next方法来遍历:

while (iter.hasNext)
  对 iter.next() 执行某种操作

这里要注意迭代器多指向的位置。在调用了map、filter、count、sum、length等方法后,迭代器将位于集合的尾端,你不能再继续使用它。而对于其他方法而言,比如find或take,迭代器位于已经找到元素或已取得元素之后。


流是一个尾部被懒计算的不可变列表—–也就是说,只有当你需要时它才会被计算。

def numsFrom(n: BigInt): Stream[BigInt] = n #:: numsFrom(n + 1)
val tenOrMore = numsFrom(10)  // 得到一个 Stream(10, ?) 流对象,尾部未被求值
temOrMore.tail.tail.tail   // Stream(13, ?)

流的方法是懒执行的。例如:

val squares = numsFrom(1).map(x => x * x)   // 产出 Stream(1, ?)
// 需要调用squares.tail来强制对下一个元素求值
squares.tail   // Stream(4, ?)

// 使用take和force强制求指定数量的值
squares.take(5).force  // Stream(1,4,9,16,25)

注意: 不要直接使用 squares.force, 这样将会是一个无穷的流的所有成员求值, 引发OutOfMemoryError 。


懒视图

应用view方法也可以实现懒执行,该方法产出一个其方法总是被懒执行的集合。例如:

val powers = (0 until 1000).view.map(pow(10, _))
powers(100)  //pow(10, 100)被计算,其他值的幂没有被计算

和流不同,view连第一个元素都不求值,除非你主动计算。view不缓存任何值,每次调用都要重新计算。

懒集合对于处理以多种方式进行变换的大型集合很有好处,因为它避免了构建出大型中间集合的需要。例如:

(0 to 1000).map(pow(10, _)).map(1 / _)  // 1
(0 to 1000).view.map(pow(10, _)).map(1 / _).force  // 2

第一个式子 会产出两个集合,第一个集合的每一个元素是pow(10, n),第二个集合是第一个集合中每个集合中的元素取倒数。 而第二个表达式使用了视图view,当动作被强制执行时,对每个元素,这两个操作是同时执行的,不需要额外构建中间集合。


与Java集合的互操作


线程安全的集合

Scala类库提供了六个特质,你可以将它们混入集合,让集合的操作变成同步:

SynchronizedBuffer

SynchronizedMap

SynchronizedPriorityQueue

SynchronizedQueue

SynchronizedSet

SynchronizedStack

例如:

val scores = new scala.collection.mutable.HashMap[String, Int] with scala.collection.mutable.SynchronizedMap[String, Int]

当然,还有更高效的集合,例如ConcurrentHashMap或ConcurrentSkipListMap,比简单的用同步方式执行所有的方法更为有效。


并行集合

集合的par方法产出当前集合的一个并行实现,例如sum求和,多个线程可以并发的计算不同区块的和,在最后这部分结果被汇总到一起。

coll.par.sum

你可以通过对要遍历的集合应用par并行for循环

for(i <- (0 until 100).par) print(i + " ")

而在 for/yield循环中,结果是依次组装的:

for(i <- (0 until 100).par) yield i + " "

这里要注意变量是共享变量,还是循环内的局部变量:

var count = 0
for (c <- coll.par) {if (c % 2 == 0) count += 1}  // error

**注意: **par方法返回的并行集合的类型为扩展自ParSeq、ParSet或ParMap特质的类型,所有这些特质都是ParIterable的子类型。这些并不是Iterable的子类型,因此你不能将并行集合传递给预期Iterable、Seq、Set、Map方法。你可以用ser方法将并行集合转换回串行集合,也可以实现接受通用的GenIterable、GenSeq、GenSeq、GenMap类型的参数的方法。

说明: 并不是所有的方法都可以被并行化。例如reduceLeft、reduceRight要求每个操作符按照顺序先后被应用。

时间: 2024-07-29 10:30:17

快学Scala第13章----集合的相关文章

快学Scala 第13章 集合 - 练习解答

1. 编写一个函数,给定字符串,产出一个包含所有字符的下标的映射.举例来说:indexes("Mississippi")应返回一个映射,让'M'对应集{0},'i'对应集{1,4,7,10},依此类推. 使用字符到可变集的映射.另外,你如何保证集是经过排序的? 回答:使用SortedSet可以保证集是经过排序的. package ex13_01 import scala.collection.mutable.SortedSet import scala.collection.mutab

快学scala 第十一章 操作符 读书笔记及习题答案代码

chapter 11 操作符 标签:快学scala 一.笔记 scala种可以在反引号中包含几乎任何字符序列, val 'val' = 42 所有的操作符都是左结合的,除了以冒号(:)结尾的操作符,和赋值操作符.用于构造列表的::操作符是又结合的.1::2::Ni1的意思是1::(2::Ni1),先创建出包含2的列表,这个列表又被作为尾巴拼接到以1作为头部的列表中. 2. 函数调用语法:f(arg1, arg2,...)扩展到可以应用于函数之外的值,如果f不是函数或方法,那么这个表达式等于f.a

快学Scala第2章–控制结构和函数 笔记

条件表达式 在Scala中,if/else 表达式是有值的,这个就是跟在if或者else之后的表达式的值.例如: val s = if(x > 0) 1 else -1 // 类似于 var s = 0 if(x > 0) s = 1 else s = -1 Scala允许使用混合类型的返回值,例如: if(x > 0) "positive" else -1 上式表达式返回的类型是它们类型的公共超类型, 在这里java.lang.String 和 Int 它们的公共超

快学Scala第10章----特质

本章要点 类可以实现任意数量的特质 特质可以要求实现它们的类具备特定的字段.方法或超类 和Java接口不同,Scala特质可以提供方法和字段的实现 当你将多个特质叠加在一起时,顺序很重要--其方法先被执行的特质排在更后面 为什么没有多重继承 Scala和Java一样不允许类从多个超类继承:从多了超类继承可能会导致许多问题,例如两个超类有相同的方法,子类该如何使用和菱形继承.在java 中类只能扩展自一个超类,它可以实现任意数量的接口,但接口只能包含抽象方法,不能包含字段. Scala提供了特质(

快学Scala第14章----模式匹配和样例类

本章要点 match表达式是一个更好的switch,不会有意外掉入到下一个分支的问题. 如果没有模式能够匹配,会抛出MatchError.可以用case _ 模式来避免. 模式可以包含一个随意定义的条件,称作守卫. 你可以对表达式的类型进行匹配:优先选择模式匹配而不是isInstanceOf/asInstanceOf. 你可以匹配数组.元组和样例类的模式,然后将匹配到的不同部分绑定到变量. 在for表达式中,不能匹配的情况会被安静的跳过. 样例类继承层级中的公共超类应该是sealed的. 用Op

快学Scala 第6章 对象 - 练习

1. 编写一个Conversions对象,加入inchesToCentimeters.gallonsToLiters和milesToKilometers方法. object Conversions {     def main(args: Array[String]){         printf("1 inch = %g centimeters\n", inchesToCentimeters(1))         printf("2 gallons = %g liter

快学Scala 第18章 高级类型 习题解答

1. 实现一个Bug类,对沿着水平线爬行的虫子建模.move方法向当前方向移动,turn方法让虫子转身,show方法打印出当前的位置.让这些方法可以被串接调用.例如: bugsy.move(4).show().move(6).show().turn().move(5).show() 上述代码应显示 4 10 5. package ex18_01 class Bug {   var x = 0   var y = 0   var curr_direction = 0   def move(len:

快学Scala 第16章 XML处理 习题解答

1. <fred/>(0) 得到什么?<fred/>(0)(0)呢?为什么? 回答:<fred/>(0) 得到一个scala.xml.Node,<fred/>(0)(0)也是得到scala.xml.Node. 因为scala.xml.Node 实现了方法 def apply(i: Int): Node,所以支持串接调用. 注意:scala-xml-x.x.x.jar 需要另外导入. scala> val a = <fred/> a: sca

快学Scala 第17章 - 类型参数 习题解答

1. 定义一个不可变类Pair[T,S],带一个swap方法,返回组件交换过位置的新对偶. package ex17_01 object Main extends App {   val p = new Pair(97 -> 'a')   val a = p.swap   println(a) } class Pair[T, S](val p: (T, S)) {   def swap = {     (p._2, p._1)   } } /*output: (a,97) */ 2. 定义一个可