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