1. 程式人生 > >Scala 集合(二)

Scala 集合(二)

Scala 通用、可變、不可變、併發以及並行集合

集合包介紹

1 scala.collection包

在collection 包中宣告的型別定義了可變及不可變的序列、可變及不可變的並行、併發集合型別共享的抽象,其中有的不僅宣告,而是直接進行了定義。這就意味著,比如只能在可變型別中使用的帶破壞性的(可變的)操作不是在這裡定義的。不過,在執行時如果集合是可變的,我們可能要考慮執行緒的安全問題。 從Predef 得到的Seq 型別是collection.Seq, 而Predef 引入的其他公共型別是以collection.immutable 開頭的,如List、Map 和Set。Predef 使用collection.Seq 的原因是要讓Scala可以像處理序列一樣處理Java 的陣列,而Java 的陣列是可變的(Predef 事實上定義了從Java 陣列到collection.mutable.ArrayOps的隱式轉換,而後者支援序列的相關操作)。Scala計劃在未來的版本中用不可變的Seq 代替它。不幸的是,就目前而言,這也意味著如果一個方法宣告它返回一個序列,它可能會返回一個可變的序列例項。同樣地,如果一個方法需要一個序列引數,呼叫者也可以傳入一個可變的序列例項。如果你更喜歡用更安全的immutable.Seq 作為預設的Seq,常見的方法是,為你的專案定義一個包物件,其中定義了Seq 型別,以覆蓋Predef 定義的Seq,如下所示:

package cn.com.tengen.test.obj
package object safeseq {
  type Seq[T] = collection.immutable.Seq[T]
}
//import  cn.com.tengen.test.obj.safeseq._
class Test {
}

object Test extends App {

  val s1: Seq[Int] = List(1,2,3,4)
  println(s1)
  val s2: Seq[Int] = Array(1,2,3,4)
  println(s2)

}

程式中導包的哪一行,註釋一旦放開,程式就會報錯

放到命令列在執行一遍

 

前兩個Seq 是由Predef 暴露的預設項collection.Seq。第一個Seq 引用了一個不可變列表,第二個Seq 引用了可變的(經過包裝的)Java 陣列。然後我們匯入了新的Seq 定義,從而遮蔽了Predef 中的Seq 定義。在重新建立Seq型別之前,引用的是 type Seq[+A] = scala.collection.Seq[A],重新定義之後引用的是collection.immutable.Seq[T]。無論哪種方式,如果我們想取集合的前幾個元素或希望從集合的一端遍歷到另一端,Seq都是具體集合的一個方便、好用的抽象。

2 collection.concurrent包

這個包只定義了兩種型別:collection.concurrent.Map特徵和實現了該trait 的collection.concurrent.TrieMap類。Map 繼承了collection.mutable.Map,但它使用了原子操作,因此得以支援執行緒安全的併發訪問。collection.mutable.Map的實現是一個字典樹雜湊類collection.concurrent.TrieMap。它實現了併發、無鎖的雜湊陣列,其目的是支援可伸縮的併發插入和刪除操作,並提高記憶體使用效率。

3 collection.convert包

在這個包中定義的型別是用來實現隱式轉換方法的,隱式轉換將Scala 的集合包裝為Java集合,反之亦然。

4 collection.generic包

collection 包宣告的抽象適用於所有集合,而collection.generic 只為實現特定的可變、不可變、並行及併發集合提供一些元件。這裡的大多數型別只對集合的實現者有意義。

5 collection.immutable包

大部分時間你都會與immutable 包中定義的集合打交道。這些型別提供了單執行緒(與並行相對)操作,由於型別是不可變的,因而是執行緒安全的。

BitSet 非負整數的集合,記憶體效率高。元素表示為可變大小的位元陣列,其中位元被打包為64 位元的字。最大元素個數決定了記憶體佔用量
HashMap 用字典雜湊實現的對映表
HashSet 用字典雜湊實現的集合
List 用於相連列表的trait,頭節點訪問複雜度為O(1),其他元素為 O(n)。其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造List 的子類例項
ListMap 用列表實現的不可變對映表
ListSet 用列表實現的不可變集合
Map 為所有不可變的對映表定義的trait,隨機訪問複雜度為O(1),其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造其子類例項
Nil 用來表示空列表的物件
NumericRange Range 類的推廣版本,將適用範圍推廣到任意完整的型別。使用時,必須提供型別的完整實現
Queue 不可變的FIFO(先入先出)佇列
Seq 為不可變序列定義的trait,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造其子類例項
Set 特徵,為不可變集合定義了操作,其伴隨物件有apply方法和其他“工廠”方法,可以用來構造其子類例項
SortedMap 為不可變對映表定義的trait,包含一個可按特定排列順序遍歷元素的迭代器。其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造其子類例項
SortedSet 為不可變集合定義的trait,包含一個可按特定排列順序遍歷元素的迭代器。其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造其子類例項
Stack 不可變的LIFO(後入先出)棧
Stream 對元素惰性求值的列表,可以支援擁有無限個潛在元素的序列
TreeMap 不可變對映表,底層用紅黑樹實現,操作的複雜度為O(log(n))
TreeSet 不可變集合,底層用紅黑樹實現,操作的複雜度為O(log(n))
Vector 不可變、支援下標的序列的預設實現

6 scala.collection.mutable包

有些時候你需要一個在單執行緒操作中的可變集合型別。我們已經討論了不可變集合為何應該成為預設選項的問題。對這些集合做可變操作不是執行緒安全的。然而,為了提高效能等原因,有原則、謹慎地使用可變資料也是恰當的。

AnyRefMap 為AnyRef 型別的鍵準備的對映表,採用開放地址法解決衝突。大部分操作通常都比HashMap 快
ArrayBuffer 內部用陣列實現的緩衝區類,追加、更新與隨機訪問的均攤時間複雜度為O(1),頭部插入和刪除操作的複雜度為O(n)
ArrayOps Java 陣列的包裝類,實現了序列操作
ArrayStack 陣列實現的棧,比通用的棧速度快
BitSet 記憶體效率高的非負整數集合
HashMap 基於散列表的可變版本的對映
HashSet 基於散列表的可變版本的集合
HashTable 用於實現基於散列表的可變集合的trait
ListMap 基於列表實現的對映
LinkedHashMap 基於散列表實現的對映,元素可以按其插入順序進行遍歷
LinkedHashSet 基於散列表實現的集合,元素可以按其插入順序進行遍歷
LongMap 鍵的型別為Long,基於散列表實現的可變對映,採用開放地址法解決衝突。大部分操作都比HashMap 快
Map Map 特徵的可變版,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造
List 的子類例項
MultiMap 可變的對映,可以對同一個鍵賦以多個值
PriorityQueue 基於堆的,可變優先佇列。對於型別為A 的元素,必須存在隱含的Ordering[A] 例項。
Queue 可變的FIFO(先入先出)佇列
Seq 表示可變序列的trait,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造
List 的子類例項
Set 聲明瞭可變集合相關操作的trait,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造List 的子類例項
SortedSet 表示可變集合的trait,包含一個可按特定排列順序遍歷元素的迭代器。其伴隨物件有
apply 方法和其他“工廠”方法,可以用來構造List 的子類例項
Stack 可變的LIFO(後入先出)棧
TreeSet 可變集合,底層用紅黑樹實現,操作的複雜度為O(log(n))
WeakHashMap 可變的雜湊對映,引用元素時採用弱引用。當元素不再有強引用時,就會被刪除。該類包裝了WeakHashMap
WrappedArray Java 陣列的包裝類,支援序列的操作
AnyRefMap 為AnyRef 型別的鍵準備的對映表,採用開放地址法解決衝突。大部分操作通常都比HashMap 快
ArrayBuffer 內部用陣列實現的緩衝區類,追加、更新與隨機訪問的均攤時間複雜度為O(1),頭部插入和刪除操作的複雜度為O(n)
ArrayOps Java 陣列的包裝類,實現了序列操作
ArrayStack 陣列實現的棧,比通用的棧速度快
BitSet 記憶體效率高的非負整數集合
HashMap 基於散列表的可變版本的對映
HashSet 基於散列表的可變版本的集合
HashTable 用於實現基於散列表的可變集合的trait
ListMap 基於列表實現的對映
LinkedHashMap 基於散列表實現的對映,元素可以按其插入順序進行遍歷
LinkedHashSet 基於散列表實現的集合,元素可以按其插入順序進行遍歷
LongMap 鍵的型別為Long,基於散列表實現的可變對映,採用開放地址法解決衝突。大部分操作都比HashMap 快
Map Map 特徵的可變版,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造
List 的子類例項
MultiMap 可變的對映,可以對同一個鍵賦以多個值
PriorityQueue 基於堆的,可變優先佇列。對於型別為A 的元素,必須存在隱含的Ordering[A] 例項。
Queue 可變的FIFO(先入先出)佇列
Seq 表示可變序列的trait,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造
List 的子類例項
Set 聲明瞭可變集合相關操作的trait,其伴隨物件有apply 方法和其他“工廠”方法,可以用來構造List 的子類例項
SortedSet 表示可變集合的trait,包含一個可按特定排列順序遍歷元素的迭代器。其伴隨物件有
apply 方法和其他“工廠”方法,可以用來構造List 的子類例項
Stack 可變的LIFO(後入先出)棧
TreeSet 可變集合,底層用紅黑樹實現,操作的複雜度為O(log(n))
WeakHashMap 可變的雜湊對映,引用元素時採用弱引用。當元素不再有強引用時,就會被刪除。該類包裝了WeakHashMap
WrappedArray Java 陣列的包裝類,支援序列的操作

WrappedArray 與ArrayOps 差不多完全相同,差別僅在於它們各自返回Array 的方法上。對於ArrayOps,返回的是新的Array[T],而WrappedArray 返回的是新的WrappedArray[T]。所以,如果使用者需要Array,更適合用ArrayOps;但當用戶並不關心這一點的時候,如果涉及序列轉換,使用WrappedArray 會更加高效。這是因為WrappedArray 避免了ArrayOps中對陣列的“打包”和“分拆”工作。

7 scala.collection.parallel包

並行集合的思想是利用現代多核系統提供的並行硬體多執行緒。根據定義,任何可以並行指定的集合操作都可以利用這種並行性。具體地說,集合被分成多個片段,操作(如map)應用在各個片段上,然後將結果組合在一起,形成最終結果。也就是說,這裡用了分而治之的策略。

在實踐中,並行集合沒有被廣泛使用,因為在許多情況下,並行化的開銷可能會掩蓋它的優點,而且不是所有的操作都可以並行執行。開銷包括執行緒排程、資料分塊、以及最後對結果的合併。通常情況下,除非該集合規模極大,否則序列執行速度會更快。所以,一定要仔細評估真實世界裡的場景,確定集合是否足夠大,並行操作是否足夠快,來讓我們選擇並行集合。

對於具體的並行集合型別,你可以直接用與非並行集合相同的慣例來例項化它,也可以對相應的非並行集合呼叫par 方法。並行集合的組合也與非並行集合類似。它們在scala.collection.parallel 包中具有共同的trait 和類,在immutable 子包中定義了相同的不可變具體集合,在mutable 子包中定義了相同的可變具體集合。

最後,有一點有必要理解,並行意味著巢狀操作的順序是未定義的。考慮如下示例,我們將從1 到10 的數字連線起來放進一個字串中:

object Test extends App {
  val t1 = ((1 to 30) fold "") ((s1, s2) => s"$s1-$s2")
  val t2 = ((1 to 30) fold "") ((s1, s2) => s"$s1-$s2")
  val t3 = ((1 to 30).par fold "") ((s1, s2) => s"$s1-$s2")
  val t4 = ((1 to 30).par fold "") ((s1, s2) => s"$s1-$s2")
  println(t1)
  println(t2)
  println(t3)
  println(t4)
}

輸出:

-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-25-26-27-28-29-30
-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-25-26-27-28-29-30
-1--2--3--4--5--6--7--8--9--10--11--12--13--14-15--16--17-18--19-20-21-22--23--24--25-26--27-28-29-30
-1--2-3--4--5--6--7--8--9--10-11--12--13--14--15--16--17-18--19-20-21-22--23-24-25-26-27-28-29-30

t1與t2的計算過程是一樣的,因為是單執行緒,所以結果也一樣

t3與t4的計算過程是一樣的,因為是多執行緒,所以結果有可能就一樣

 

但對於求和運算,與多少個執行緒沒關係:

object Test extends App {
  val t1 = (1 to 300) reduce (_ + _)
  val t2 = (1 to 300)reduce (_ + _)
  val t3 = (1 to 300).par reduce (_ + _)
  val t4 = (1 to 300).par reduce (_ + _)
  println(t1)
  println(t2)
  println(t3)
  println(t4)
}

執行結果:

45150
45150
45150
45150