1. 程式人生 > >快學Scala第13章----集合

快學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要求每個操作符按照順序先後被應用。