簡介(Introduction)
Martin Odersky和Lex Spoon
在許多人看來,新的集合框架是Scala 2.8中最顯著的改進。此前Scala也有集合(實際上新框架大部分地兼容了舊框架),但2.8中的集合類在通用性、一致性和功能的豐富性上更勝一籌。
即使粗看上去集合新增的內容比較微妙,但這些改動卻足以對開發者的編程風格造成深遠的影響。實際上,就好像你從事一個高層次的程序,而此程序的基本的構建塊的元素被整個集合代替。適應這種新的編程風格需要一個過程。幸運的是,新的Scala集合得益於幾個新的幾個漂亮的屬性,從而它們易於使用、簡潔、安全、快速、通用。
-
易用性:由20-50個方法的小詞彙量,足以在幾個操作內解決大部分的集合問題。沒有必要被複雜的循環或遞歸所困擾。持久化的集合類和無副作用的操作意味着你不必擔心新數據會意外的破壞已經存在的集合。迭代器和集合更新之間的干擾會被消除!
-
簡潔:你可以通過單獨的一個詞來執行一個或多個循環。你也可以用輕量級的語法和組合輕鬆地快速表達功能性的操作,以致結果看起來像一個自定義的代數。
-
安全:這一問題必須被熟練的理解,Scala集合的靜態類型和函數性質意味着你在編譯的時候就可以捕獲絕大多數錯誤。原因是(1)、集合操作本身被大量的使用,是測試良好的。(2)、集合的用法要求輸入和輸出要顯式作爲函數參數和結果。(3)這些顯式的輸入輸出會受到靜態類型檢查。最終,絕大部分的誤用將會顯示爲類型錯誤。這是很少見的的有數百行的程序的首次運行。
-
快速:集合操作已經在類庫裏是調整和優化過。因此,使用集合通常是比較高效的。你也許能夠通過手動調整數據結構和操作來做的好一點,但是你也可能會由於一些次優的實現而做的更糟。不僅如此,集合類最近已經能支持在多核處理器上並行運算。並行集合類支持有序集合的相同操作,因此沒有新的操作需要學習也沒有代碼需要重寫。你可以簡單地通過調用標準方法來把有序集合優化爲一個並行集合。
-
通用:集合類提供了相同的操作在一些類型上,確實如此。所以,你可以用相當少的詞彙完成大量的工作。例如,一個字符串從概念上講就是一個字符序列。因此,在Scala集合類中,字符串支持所有的序列操作。同樣的,數組也是支持的。
例子:這有一行代碼演示了Scala集合類的先進性。
val (minors, adults) = people partition (_.age < 18)
這個操作是清晰的:通過他們的age(年齡)把這個集合people拆分到到miors(未成年人)和adults(成年人)中。由於這個拆分方法是被定義在根集合類型TraversableLike類中,這部分代碼服務於任何類型的集合,包括數組。例子運行的結果就是miors和adults集合與people集合的類型相同。
這個代碼比使用傳統的類運行一到三個循環更加簡明(三個循環處理一個數組,是由於中間結果需要有其它地方做緩存)。一旦你已經學習了基本的集合詞彙,你將也發現寫這種代碼顯式的循環更簡單和更安全。而且,這個拆分操作是非常快速,並且在多核處理器上採用並行集合類達到更快的速度(並行集合類已經Scala 2.9的一部分發布)。
本文檔從一個用戶的角度出發,提供了一個關於Scala集合類的 API的深入討論。它將帶你體驗它定義的所有的基礎類和方法。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/introduction.html
Mutable 和 Immutable 集合
Scala 集合類系統地區分了可變的和不可變的集合。可變集合可以在適當的地方被更新或擴展。這意味着你可以修改,添加,移除一個集合的元素。而不可變集合類,相比之下,永遠不會改變。不過,你仍然可以模擬添加,移除或更新操作。但是這些操作將在每一種情況下都返回一個新的集合,同時使原來的集合不發生改變。
所有的集合類都可以在包scala.collection
或scala.collection.mutable
,scala.collection.immutable
,scala.collection.generic
中找到。客戶端代碼需要的大部分集合類都獨立地存在於3種變體中,它們位於scala.collection
, scala.collection.immutable
, scala.collection.mutable
包。每一種變體在可變性方面都有不同的特徵。
scala.collection.immutable
包是的集合類確保不被任何對象改變。例如一個集合創建之後將不會改變。因此,你可以相信一個事實,在不同的點訪問同一個集合的值,你將總是得到相同的元素。。
scala.collection.mutable
包的集合類則有一些操作可以修改集合。所以處理可變集合意味着你需要去理解哪些代碼的修改會導致集合同時改變。
scala.collection
包中的集合,既可以是可變的,也可以是不可變的。例如:collection.IndexedSeq[T]]
就是 collection.immutable.IndexedSeq[T] 和collection.mutable.IndexedSeq[T]這兩類的超類。scala.collection
包中的根集合類中定義了相同的接口作爲不可變集合類,同時,scala.collection.mutable
包中的可變集合類代表性的添加了一些有輔助作用的修改操作到這個immutable
接口。
根集合類與不可變集合類之間的區別是不可變集合類的客戶端可以確保沒有人可以修改集合。然而,根集合類的客戶端僅保證不修改集合本身。即使這個集合類沒有提供修改集合的靜態操作,它仍然可能在運行時作爲可變集合被其它客戶端所修改。
默認情況下,Scala 一直採用不可變集合類。例如,如果你僅寫了Set
而沒有任何加前綴也沒有從其它地方導入Set
,你會得到一個不可變的set
,另外如果你寫迭代,你也會得到一個不可變的迭代集合類,這是由於這些類在從scala中導入的時候都是默認綁定的。爲了得到可變的默認版本,你需要顯式的聲明collection.mutable.Set
或collection.mutable.Iterable
.
一個有用的約定,如果你想要同時使用可變和不可變集合類,只導入collection.mutable包即可。
import scala.collection.mutable //導入包scala.collection.mutable
然而,像沒有前綴的Set這樣的關鍵字, 仍然指的是一個不可變集合,然而mutable.Set
指的是可變的副本(可變集合)。
集合樹的最後一個包是collection.generic
。這個包包含了集合的構建塊。集合類延遲了collection.generic
類中的部分操作實現,另一方面集合框架的用戶需要引用collection.generic
中類在異常情況中。
爲了方便和向後兼容性,一些導入類型在包scala中有別名,所以你能通過簡單的名字使用它們而不需要import。這有一個例子是List 類型,它可以用以下兩種方法使用,如下:
scala.collection.immutable.List // 這是它的定義位置
scala.List //通過scala 包中的別名
List // 因爲scala._
// 總是是被自動導入。
其它類型的別名有: Traversable, Iterable, Seq, IndexedSeq, Iterator, Stream, Vector, StringBuilder, Range。
下面的圖表顯示了scala.collection
包中所有的集合類。這些都是高級抽象類或特性,它們通常具備和不可變實現一樣的可變實現。
下面的圖表顯示scala.collection.immutable中的所有集合類。
下面的圖表顯示scala.collection.mutable中的所有集合類。
(以上三個圖表由Matthias生成, 來自decodified.com)。
集合API概述
大多數重要的集合類都被展示在了上表。而且這些類有很多的共性。例如,每一種集合都能用相同的語法創建,寫法是集合類名緊跟着元素。
Traversable(1, 2, 3)
Iterable("x", "y", "z")
Map("x" -> 24, "y" -> 25, "z" -> 26)
Set(Color.red, Color.green, Color.blue)
SortedSet("hello", "world")
Buffer(x, y, z)
IndexedSeq(1.0, 2.0)
LinearSeq(a, b, c)
相同的原則也應用於特殊的集合實現,例如:
List(1, 2, 3)
HashMap("x" -> 24, "y" -> 25, "z" -> 26)
所有這些集合類都通過相同的途徑,用toString方法展示出來。
Traversable類提供了所有集合支持的API,同時,對於特殊類型也是有意義的。例如,Traversable類 的map方法會返回另一個Traversable對象作爲結果,但是這個結果類型在子類中被重寫了。例如,在一個List上調用map會又生成一個List,在Set上調用會再生成一個Set,以此類推。
scala> List(1, 2, 3) map (_ + 1)
res0: List[Int] = List(2, 3, 4)
scala> Set(1, 2, 3) map (_ * 2)
res0: Set[Int] = Set(2, 4, 6)
在集合類庫中,這種在任何地方都實現了的行爲,被稱之爲返回類型一致原則。
大多數類在集合樹中存在這於三種變體:root, mutable 和immutable。唯一的例外是緩衝區特徵,它僅在於mutable集合。
下面我們將一個個的回顧這些類,更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/overview.html
Trait Traversable
Traversable(遍歷)是容器(collection)類的最高級別特性,它唯一的抽象操作是foreach:
def foreach[U](/DOC_Scala/chinese_scala_offical_document/file/f: Elem => U)
需要實現Traversable的容器(collection)類僅僅需要定義與之相關的方法,其他所有方法可都可以從Traversable中繼承。
foreach方法用於遍歷容器(collection)內的所有元素和每個元素進行指定的操作(比如說f操作)。操作類型是Elem => U,其中Elem是容器(collection)中元素的類型,U是一個任意的返回值類型。對f的調用僅僅是容器遍歷的副作用,實際上所有函數f的計算結果都被foreach拋棄了。
Traversable同時定義的很多具體方法,如下表所示。這些方法可以劃分爲以下類別:
-
相加操作++(addition)表示把兩個traversable對象附加在一起或者把一個迭代器的所有元素添加到traversable對象的尾部。
-
Map操作有map,flatMap和collect,它們可以通過對容器中的元素進行某些運算來生成一個新的容器。
-
轉換器(Conversion)操作包括toArray,toList,toIterable,toSeq,toIndexedSeq,toStream,toSet,和toMap,它們可以按照某種特定的方法對一個Traversable 容器進行轉換。等容器類型已經與所需類型相匹配的時候,所有這些轉換器都會不加改變的返回該容器。例如,對一個list使用toList,返回的結果就是list本身。
-
拷貝(Copying)操作有copyToBuffer和copyToArray。從字面意思就可以知道,它們分別用於把容器中的元素元素拷貝到一個緩衝區或者數組裏。
-
Size info操作包括有isEmpty,nonEmpty,size和hasDefiniteSize。Traversable容器有有限和無限之分。比方說,自然數流Stream.from(0)就是一個無限的traversable 容器。hasDefiniteSize方法能夠判斷一個容器是否可能是無限的。若hasDefiniteSize返回值爲ture,容器肯定有限。若返回值爲false,根據完整信息才能判斷容器(collection)是無限還是有限。
-
元素檢索(Element Retrieval)操作有head,last,headOption,lastOption和find。這些操作可以查找容器的第一個元素或者最後一個元素,或者第一個符合某種條件的元素。注意,儘管如此,但也不是所有的容器都明確定義了什麼是“第一個”或”最後一個“。例如,通過哈希值儲存元素的哈希集合(hashSet),每次運行哈希值都會發生改變。在這種情況下,程序每次運行都可能會導致哈希集合的”第一個“元素髮生變化。如果一個容器總是以相同的規則排列元素,那這個容器是有序的。大多數容器都是有序的,但有些不是(例如哈希集合)-- 排序會造成一些額外消耗。排序對於重複性測試和輔助調試是不可或缺的。這就是爲什麼Scala容器中的所有容器類型都把有序作爲可選項。例如,帶有序性的HashSet就是LinkedHashSet。
-
子容器檢索(sub-collection Retrieval)操作有tail,init,slice,take,drop,takeWhilte,dropWhile,filter,filteNot和withFilter。它們都可以通過範圍索引或一些論斷的判斷返回某些子容器。
-
拆分(Subdivision)操作有splitAt,span,partition和groupBy,它們用於把一個容器(collection)裏的元素分割成多個子容器。
-
元素測試(Element test)包括有exists,forall和count,它們可以用一個給定論斷來對容器中的元素進行判斷。
-
摺疊(Folds)操作有foldLeft,foldRight,/:,:\,reduceLeft和reduceRight,用於對連續性元素的二進制操作。
-
特殊摺疊(Specific folds)包括sum, product, min, max。它們主要用於特定類型的容器(數值或比較)。
-
字符串(String)操作有mkString,addString和stringPrefix,可以將一個容器通過可選的方式轉換爲字符串。
-
視圖(View)操作包含兩個view方法的重載體。一個view對象可以當作是一個容器客觀地展示。接下來將會介紹更多有關視圖內容。
Traversable對象的操作
WHAT IT IS | WHAT IT DOES |
---|---|
抽象方法: | |
xs foreach f | 對xs中的每一個元素執行函數f |
加運算(Addition): | |
xs ++ ys | 生成一個由xs和ys中的元素組成容器。ys是一個TraversableOnce容器,即Taversable類型或迭代器。 |
Maps: | |
xs map f | 通過函數xs中的每一個元素調用函數f來生成一個容器。 |
xs flatMap f | 通過對容器xs中的每一個元素調用作爲容器的值函數f,在把所得的結果連接起來作爲一個新的容器。 |
xs collect f | 通過對每個xs中的符合定義的元素調用偏函數f,並把結果收集起來生成一個集合。 |
轉換(Conversions): | |
xs.toArray | 把容器轉換爲一個數組 |
xs.toList | 把容器轉換爲一個list |
xs.toIterable | 把容器轉換爲一個迭代器。 |
xs.toSeq | 把容器轉換爲一個序列 |
xs.toIndexedSeq | 把容器轉換爲一個索引序列 |
xs.toStream | 把容器轉換爲一個延遲計算的流。 |
xs.toSet | 把容器轉換爲一個集合(Set)。 |
xs.toMap | 把由鍵/值對組成的容器轉換爲一個映射表(map)。如果該容器並不是以鍵/值對作爲元素的,那麼調用這個操作將會導致一個靜態類型的錯誤。 |
拷貝(Copying): | |
xs copyToBuffer buf | 把容器的所有元素拷貝到buf緩衝區。 |
xs copyToArray(arr, s, n) | 拷貝最多n個元素到數組arr的座標s處。參數s,n是可選項。 |
大小判斷(Size info): | |
xs.isEmpty | 測試容器是否爲空。 |
xs.nonEmpty | 測試容器是否包含元素。 |
xs.size | 計算容器內元素的個數。 |
xs.hasDefiniteSize | 如果xs的大小是有限的,則爲true。 |
元素檢索(Element Retrieval): | |
xs.head | 返回容器內第一個元素(或其他元素,若當前的容器無序)。 |
xs.headOption | xs選項值中的第一個元素,若xs爲空則爲None。 |
xs.last | 返回容器的最後一個元素(或某個元素,如果當前的容器無序的話)。 |
xs.lastOption | xs選項值中的最後一個元素,如果xs爲空則爲None。 |
xs find p | 查找xs中滿足p條件的元素,若存在則返回第一個元素;若不存在,則爲空。 |
子容器(Subcollection): | |
xs.tail | 返回由除了xs.head外的其餘部分。 |
xs.init | 返回除xs.last外的其餘部分。 |
xs slice (from, to) | 返回由xs的一個片段索引中的元素組成的容器(從from到to,但不包括to)。 |
xs take n | 由xs的第一個到第n個元素(或當xs無序時任意的n個元素)組成的容器。 |
xs drop n | 由除了xs take n以外的元素組成的容器。 |
xs takeWhile p | 容器xs中最長能夠滿足斷言p的前綴。 |
xs dropWhile p | 容器xs中除了xs takeWhile p以外的全部元素。 |
xs filter p | 由xs中滿足條件p的元素組成的容器。 |
xs withFilter p | 這個容器是一個不太嚴格的過濾器。子容器調用map,flatMap,foreach和withFilter只適用於xs中那些的滿足條件p的元素。 |
xs filterNot p | 由xs中不滿足條件p的元素組成的容器。 |
拆分(Subdivision): | |
xs splitAt n | 把xs從指定位置的拆分成兩個容器(xs take n和xs drop n)。 |
xs span p | 根據一個斷言p將xs拆分爲兩個容器(xs takeWhile p, xs.dropWhile p)。 |
xs partition p | 把xs分割爲兩個容器,符合斷言p的元素賦給一個容器,其餘的賦給另一個(xs filter p, xs.filterNot p)。 |
xs groupBy f | 根據判別函數f把xs拆分一個到容器(collection)的map中。 |
條件元素(Element Conditions): | |
xs forall p | 返回一個布爾值表示用於表示斷言p是否適用xs中的所有元素。 |
xs exists p | 返回一個布爾值判斷xs中是否有部分元素滿足斷言p。 |
xs count p | 返回xs中符合斷言p條件的元素個數。 |
摺疊(Fold): | |
(z /: xs)(op) | 在xs中,對由z開始從左到右的連續元素應用二進制運算op。 |
(xs :\ z)(op) | 在xs中,對由z開始從右到左的連續元素應用二進制運算op |
xs.foldLeft(z)(op) | 與(z /: xs)(op)相同。 |
xs.foldRight(z)(op) | 與 (xs :\ z)(op)相同。 |
xs reduceLeft op | 非空容器xs中的連續元素從左至右調用二進制運算op。 |
xs reduceRight op | 非空容器xs中的連續元素從右至左調用二進制運算op。 |
特殊摺疊(Specific Fold): | |
xs.sum | 返回容器xs中數字元素的和。 |
xs.product | xs返回容器xs中數字元素的積。 |
xs.min | 容器xs中有序元素值中的最小值。 |
xs.max | 容器xs中有序元素值中的最大值。 |
字符串(String): | |
xs addString (b, start, sep, end) | 把一個字符串加到StringBuilder對象b中,該字符串顯示爲將xs中所有元素用分隔符sep連接起來並封裝在start和end之間。其中start,end和sep都是可選的。 |
xs mkString (start, sep, end) | 把容器xs轉換爲一個字符串,該字符串顯示爲將xs中所有元素用分隔符sep連接起來並封裝在start和end之間。其中start,end和sep都是可選的。 |
xs.stringPrefix | 返回一個字符串,該字符串是以容器名開頭的xs.toString。 |
視圖(View): | |
xs.view | 通過容器xs生成一個視圖。 |
xs view (from, to) | 生成一個表示在指定索引範圍內的xs元素的視圖。 |
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/trait-traversable.html
Trait Iterable
自下而上的容器(collection)層次結構具有可迭代的Trait。Trait的所有方法可定義爲一個抽象方法,逐個生成容器(collection)元素迭代器。Traversable Trait的foreach方法實現了迭代器的Iterable。下面是具體的實現。
def foreach[U](/DOC_Scala/chinese_scala_offical_document/file/f: Elem => U): Unit = {
val it = iterator
while (it.hasNext) f(it.next())
}
許多Iterable 的子類覆寫了Iteable的foreach標準實現,因爲它們提供了更多有效的實現。記住,由於性能問題,foreach是Traversable所有操作能夠實現的基礎。
Iterable有兩個方法返回迭代器:grouped和sliding。然而,這些迭代器返回的不是單個元素,而是原容器(collection)元素的全部子序列。這些最大的子序列作爲參數傳給這些方法。grouped方法返回元素的增量分塊,sliding方法生成一個滑動元素的窗口。兩者之間的差異通過REPL的作用能夠清楚看出。
scala> val xs = List(1, 2, 3, 4, 5)
xs: List[Int] = List(1, 2, 3, 4, 5)
scala> val git = xs grouped 3
git: Iterator[List[Int]] = non-empty iterator
scala> git.next()
res3: List[Int] = List(1, 2, 3)
scala> git.next()
res4: List[Int] = List(4, 5)
scala> val sit = xs sliding 3
sit: Iterator[List[Int]] = non-empty iterator
scala> sit.next()
res5: List[Int] = List(1, 2, 3)
scala> sit.next()
res6: List[Int] = List(2, 3, 4)
scala> sit.next()
res7: List[Int] = List(3, 4, 5)
當只有一個迭代器可用時,Trait Iterable增加了一些其他方法,爲了能被有效的實現的可遍歷的情況。這些方法總結在下面的表中。
Trait Iterable操作
WHAT IT IS | WHAT IT DOES |
---|---|
抽象方法: | |
xs.iterator | xs迭代器生成的每一個元素,以相同的順序就像foreach一樣遍歷元素。 |
其他迭代器: | |
xs grouped size | 一個迭代器生成一個固定大小的容器(collection)塊。 |
xs sliding size | 一個迭代器生成一個固定大小的滑動窗口作爲容器(collection)的元素。 |
子容器(Subcollection): | |
xs takeRight n | 一個容器(collection)由xs的最後n個元素組成(或,若定義的元素是無序,則由任意的n個元素組成)。 |
xs dropRight n | 一個容器(collection)由除了xs 被取走的(執行過takeRight ()方法)n個元素外的其餘元素組成。 |
拉鍊方法(Zippers): | |
xs zip ys | 把一對容器 xs和ys的包含的元素合成到一個iterabale。 |
xs zipAll (ys, x, y) | 一對容器 xs 和ys的相應的元素合併到一個iterable ,實現方式是通過附加的元素x或y,把短的序列被延展到相對更長的一個上。 |
xs.zip WithIndex | 把一對容器xs和它的序列,所包含的元素組成一個iterable 。 |
比對: | |
xs sameElements ys | 測試 xs 和 ys 是否以相同的順序包含相同的元素。 |
在Iterable下的繼承層次結構你會發現有三個traits:Seq,Set,和 Map。這三個Traits有一個共同的特徵,它們都實現了PartialFunction trait以及它的應用和isDefinedAt 方法。然而,每一個trait實現的PartialFunction 方法卻各不相同。
例如序列,使用用的是位置索引,它裏面的元素的總是從0開始編號。即Seq(1, 2, 3)(1)
爲2。例如sets,使用的是成員測試。例如Set('a', 'b', 'c')('b')
算出來的是true,而Set()('a')
爲false。最後,maps使用的是選擇。比如Map('a'
-> 1, 'b' -> 10, 'c' -> 100)('b')
得到的是10。
接下來,我們將詳細的介紹三種類型的容器(collection)。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/trait-iterable.html
序列trait:Seq、IndexedSeq及LinearSeq
Seq trait用於表示序列。所謂序列,指的是一類具有一定長度的可迭代訪問的對象,其中每個元素均帶有一個從0開始計數的固定索引位置。
序列的操作有以下幾種,如下表所示:
- 索引和長度的操作 apply、isDefinedAt、length、indices,及lengthCompare。序列的apply操作用於索引訪問;因此,Seq[T]類型的序列也是一個以單個Int(索引下標)爲參數、返回值類型爲T的偏函數。換言之,Seq[T]繼承自Partial Function[Int, T]。序列各元素的索引下標從0開始計數,最大索引下標爲序列長度減一。序列的length方法是collection的size方法的別名。lengthCompare方法可以比較兩個序列的長度,即便其中一個序列長度無限也可以處理。
- 索引檢索操作(indexOf、lastIndexOf、indexofSlice、lastIndexOfSlice、indexWhere、lastIndexWhere、segmentLength、prefixLength)用於返回等於給定值或滿足某個謂詞的元素的索引。
- 加法運算(+:,:+,padTo)用於在序列的前面或者後面添加一個元素並作爲新序列返回。
- 更新操作(updated,patch)用於替換原序列的某些元素並作爲一個新序列返回。
- 排序操作(sorted, sortWith, sortBy)根據不同的條件對序列元素進行排序。
- 反轉操作(reverse, reverseIterator, reverseMap)用於將序列中的元素以相反的順序排列。
- 比較(startsWith, endsWith, contains, containsSlice, corresponds)用於對兩個序列進行比較,或者在序列中查找某個元素。
- 多集操作(intersect, diff, union, distinct)用於對兩個序列中的元素進行類似集合的操作,或者刪除重複元素。
如果一個序列是可變的,它提供了另一種更新序列中的元素的,但有副作用的update方法,Scala中常有這樣的語法,如seq(idx) = elem。它只是seq.update(idx, elem)的簡寫,所以update 提供了方便的賦值語法。應注意update 和updated之間的差異。update 再原來基礎上更改序列中的元素,並且僅適用於可變序列。而updated 適用於所有的序列,它總是返回一個新序列,而不會修改原序列。
Set類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
索引和長度 | |
xs(i) | (或者寫作xs apply i)。xs的第i個元素 |
xs isDefinedAt i | 測試xs.indices中是否包含i。 |
xs.length | 序列的長度(同size)。 |
xs.lengthCompare ys | 如果xs的長度小於ys的長度,則返回-1。如果xs的長度大於ys的長度,則返回+1,如果它們長度相等,則返回0。即使其中一個序列是無限的,也可以使用此方法。 |
xs.indices | xs的索引範圍,從0到xs.length - 1。 |
索引搜索 | |
xs indexOf x | 返回序列xs中等於x的第一個元素的索引(存在多種變體)。 |
xs lastIndexOf x | 返回序列xs中等於x的最後一個元素的索引(存在多種變體)。 |
xs indexOfSlice ys | 查找子序列ys,返回xs中匹配的第一個索引。 |
xs indexOfSlice ys | 查找子序列ys,返回xs中匹配的倒數一個索引。 |
xs indexWhere p | xs序列中滿足p的第一個元素。(有多種形式) |
xs segmentLength (p, i) | xs中,從xs(i)開始並滿足條件p的元素的最長連續片段的長度。 |
xs prefixLength p | xs序列中滿足p條件的先頭元素的最大個數。 |
加法: | |
x +: xs | 由序列xs的前方添加x所得的新序列。 |
xs :+ x | 由序列xs的後方追加x所得的新序列。 |
xs padTo (len, x) | 在xs後方追加x,直到長度達到len後得到的序列。 |
更新 | |
xs patch (i, ys, r) | 將xs中第i個元素開始的r個元素,替換爲ys所得的序列。 |
xs updated (i, x) | 將xs中第i個元素替換爲x後所得的xs的副本。 |
xs(i) = x | (或寫作 xs.update(i, x),僅適用於可變序列)將xs序列中第i個元素修改爲x。 |
排序 | |
xs.sorted | 通過使用xs中元素類型的標準順序,將xs元素進行排序後得到的新序列。 |
xs sortWith lt | 將lt作爲比較操作,並以此將xs中的元素進行排序後得到的新序列。 |
xs sortBy f | 將序列xs的元素進行排序後得到的新序列。參與比較的兩個元素各自經f函數映射後得到一個結果,通過比較它們的結果來進行排序。 |
反轉 | |
xs.reverse | 與xs序列元素順序相反的一個新序列。 |
xs.reverseIterator | 產生序列xs中元素的反序迭代器。 |
xs reverseMap f | 以xs的相反順序,通過f映射xs序列中的元素得到的新序列。 |
比較 | |
xs startsWith ys | 測試序列xs是否以序列ys開頭(存在多種形式)。 |
xs endsWith ys | 測試序列xs是否以序列ys結束(存在多種形式)。 |
xs contains x | 測試xs序列中是否存在一個與x相等的元素。 |
xs containsSlice ys | 測試xs序列中是否存在一個與ys相同的連續子序列。 |
(xs corresponds ys)(p) | 測試序列xs與序列ys中對應的元素是否滿足二元的判斷式p。 |
多集操作 | |
xs intersect ys | 序列xs和ys的交集,並保留序列xs中的順序。 |
xs diff ys | 序列xs和ys的差集,並保留序列xs中的順序。 |
xs union ys | 並集;同xs ++ ys。 |
xs.distinct | 不含重複元素的xs的子序列。 |
特性(trait) Seq 具有兩個子特徵(subtrait) LinearSeq和IndexedSeq。它們不添加任何新的操作,但都提供不同的性能特點:線性序列具有高效的 head 和 tail 操作,而索引序列具有高效的apply, length, 和 (如果可變) update操作。
常用線性序列有 scala.collection.immutable.List
和scala.collection.immutable.Stream
。常用索引序列有scala.Array
scala.collection.mutable.ArrayBuffer
。Vector 類提供一個在索引訪問和線性訪問之間有趣的折中。它同時具有高效的恆定時間的索引開銷,和恆定時間的線性訪問開銷。正因爲如此,對於混合訪問模式,vector是一個很好的基礎。後面將詳細介紹vector。
緩衝器
Buffers是可變序列一個重要的種類。它們不僅允許更新現有的元素,而且允許元素的插入、移除和在buffer尾部高效地添加新元素。buffer 支持的主要新方法有:用於在尾部添加元素的 +=
和 ++=
;用於在前方添加元素的+=:
和++=:
;用於插入元素的 insert
和insertAll
;以及用於刪除元素的remove
和 -=
。如下表所示。
ListBuffer和ArrayBuffer是常用的buffer實現 。顧名思義,ListBuffer依賴列表(List),支持高效地將它的元素轉換成列表。而ArrayBuffer依賴數組(Array),能快速地轉換成數組。
Buffer類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
加法: | |
buf += x | 將元素x追加到buffer,並將buf自身作爲結果返回。 |
buf += (x, y, z) | 將給定的元素追加到buffer。 |
buf ++= xs | 將xs中的所有元素追加到buffer。 |
x +=: buf | 將元素x添加到buffer的前方。 |
xs ++=: buf | 將xs中的所有元素都添加到buffer的前方。 |
buf insert (i, x) | 將元素x插入到buffer中索引爲i的位置。 |
buf insertAll (i, xs) | 將xs的所有元素都插入到buffer中索引爲i的位置。 |
移除: | |
buf -= x | 將元素x從buffer中移除。 |
buf remove i | 將buffer中索引爲i的元素移除。 |
buf remove (i, n) | 將buffer中從索引i開始的n個元素移除。 |
buf trimStart n | 移除buffer中的前n個元素。 |
buf trimEnd n | 移除buffer中的後n個元素。 |
buf.clear() | 移除buffer中的所有元素。 |
克隆: | |
buf.clone | 與buf具有相同元素的新buffer。 |
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/seqs.html
集合(Set)
集合是不包含重複元素的可迭代對象。下面的通用集合表和可變集合表中概括了集合類型適用的運算。分爲幾類:
- 測試型的方法:contains,apply,subsetOf。contains方法用於判斷集合是否包含某元素。集合的apply方法和contains方法的作用相同,因此 set(elem) 等同於set constains elem。這意味着集合對象的名字能作爲其自身是否包含某元素的測試函數。
例如
val fruit = Set("apple", "orange", "peach", "banana")
fruit: scala.collection.immutable.Set[java.lang.String] =
Set(apple, orange, peach, banana)
scala> fruit("peach")
res0: Boolean = true
scala> fruit("potato")
res1: Boolean = false
- 加法類型的方法: + 和 ++ 。添加一個或多個元素到集合中,產生一個新的集合。
- 減法類型的方法: - 、--。它們實現從一個集合中移除一個或多個元素,產生一個新的集合。
- Set運算包括並集、交集和差集。每一種運算都存在兩種書寫形式:字母和符號形式。字母形式:intersect、union和diff,符號形式:&、|和&~。事實上,Set中繼承自Traversable的++也能被看做union或|的另一個別名。區別是,++的參數爲Traversable對象,而union和|的參數是集合。
Set 類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
實驗代碼: | |
xs contains x | 測試x是否是xs的元素。 |
xs(x) | 與xs contains x相同。 |
xs subsetOf ys | 測試xs是否是ys的子集。 |
加法: | |
xs + x | 包含xs中所有元素以及x的集合。 |
xs + (x, y, z) | 包含xs中所有元素及附加元素的集合 |
xs ++ ys | 包含xs中所有元素及ys中所有元素的集合 |
實驗代碼: | |
xs - x | 包含xs中除x以外的所有元素的集合。 |
xs - x | 包含xs中除去給定元素以外的所有元素的集合。 |
xs -- ys | 集合內容爲:xs中所有元素,去掉ys中所有元素後剩下的部分。 |
xs.empty | 與xs同類的空集合。 |
二進制操作: | |
xs & ys | 集合xs和ys的交集。 |
xs intersect ys | 等同於 xs & ys。 |
xs | ys |
xs union ys | 等同於xs |
xs &~ ys | 集合xs和ys的差集。 |
xs diff ys | 等同於 xs &~ ys。 |
可變集合提供加法類方法,可以用來添加、刪除或更新元素。下面對這些方法做下總結。
mutable.Set 類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
加法: | |
xs += x | 把元素x添加到集合xs中。該操作有副作用,它會返回左操作符,這裏是xs自身。 |
xs += (x, y, z) | 添加指定的元素到集合xs中,並返回xs本身。(同樣有副作用) |
xs ++= ys | 添加集合ys中的所有元素到集合xs中,並返回xs本身。(表達式有副作用) |
xs add x | 把元素x添加到集合xs中,如集合xs之前沒有包含x,該操作返回true,否則返回false。 |
移除: | |
xs -= x | 從集合xs中刪除元素x,並返回xs本身。(表達式有副作用) |
xs -= (x, y, z) | 從集合xs中刪除指定的元素,並返回xs本身。(表達式有副作用) |
xs --= ys | 從集合xs中刪除所有屬於集合ys的元素,並返回xs本身。(表達式有副作用) |
xs remove x | 從集合xs中刪除元素x。如之前xs中包含了x元素,返回true,否則返回false。 |
xs retain p | 只保留集合xs中滿足條件p的元素。 |
xs.clear() | 刪除集合xs中的所有元素。 |
**更新: ** | |
xs(x) = b | ( 同 xs.update(x, b) )參數b爲布爾類型,如果值爲true就把元素x加入集合xs,否則從集合xs中刪除x。 |
克隆: | |
xs.clone | 產生一個與xs具有相同元素的可變集合。 |
與不變集合一樣,可變集合也提供了+
和++
操作符來添加元素,-
和--
用來刪除元素。但是這些操作在可變集合中通常很少使用,因爲這些操作都要通過集合的拷貝來實現。可變集合提供了更有效率的更新方法,+=
和-=
。 s
+= elem
,添加元素elem到集合s中,並返回產生變化後的集合作爲運算結果。同樣的,s -= elem
執行從集合s中刪除元素elem的操作,並返回產生變化後的集合作爲運算結果。除了+=
和-=
之外還有從可遍歷對象集合或迭代器集合中添加和刪除所有元素的批量操作符++=
和--=
。
選用+=
和-=
這樣的方法名使得我們得以用非常近似的代碼來處理可變集合和不可變集合。先看一下以下處理不可變集合s的REPL會話:
scala> var s = Set(1, 2, 3)
s: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
scala> s += 4
scala> s -= 2
scala> s
res2: scala.collection.immutable.Set[Int] = Set(1, 3, 4)
我們在immutable.Set
類型的變量中使用+=
和-=
。諸如 s
+= 4
的表達式是 s = s + 4
的縮寫,它的作用是,在集合s上運用方法+
,並把結果賦回給變量s。下面我們來分析可變集合上的類似操作。
scala> val s = collection.mutable.Set(1, 2, 3)
s: scala.collection.mutable.Set[Int] = Set(1, 2, 3)
scala> s += 4
res3: s.type = Set(1, 4, 2, 3)
scala> s -= 2
res4: s.type = Set(1, 4, 3)
最後結果看起來和之前的在非可變集合上的操作非常相似;從Set(1, 2, 3)
開始,最後得到Set(1, 3, 4)
。然而,儘管相似,但它們在實現上其實是不同的。 這裏s
+= 4
是在可變集合值s上調用+=
方法,它會改變s的內容。同樣的,s -= 2
也是在s上調用 -=
方法,也會修改s集合的內容。
通過比較這兩種方式得出一個重要的原則。我們通常能用一個非可變集合的變量來替換可變集合的常量,反之亦然。這一原則至少在沒有別名的引用添加到Collection時起作用。別名引用主要是用來觀察操作在Collection上直接做的修改還是生成了一個新的Collection。
可變集合同樣提供作爲+=
和-=
的變型方法,add和remove,它們的不同之處在於add和remove會返回一個表明運算是否對集合有作用的Boolean值
目前可變集合默認使用哈希表來存儲集合元素,非可變集合則根據元素個數的不同,使用不同的方式來實現。空集用單例對象來表示。元素個數小於等於4的集合可以使用單例對象來表達,元素作爲單例對象的字段來存儲。 元素超過4個,非可變集合就用哈希前綴樹(hash trie)來實現。
採用這種表示方法,較小的不可變集合(元素數不超過4)往往會比可變集合更加緊湊和高效。所以,在處理小尺寸的集合時,不妨試試不可變集合。
集合的兩個特質是SortedSet和 BitSet。
有序集(SortedSet)
SortedSet 是指以特定的順序(這一順序可以在創建集合之初自由的選定)排列其元素(使用iterator或foreach)的集合。 SortedSet 的默認表示是有序二叉樹,即左子樹上的元素小於所有右子樹上的元素。這樣,一次簡單的順序遍歷能按增序返回集合中的所有元素。Scala的類 immutable.TreeSet
使用紅黑樹實現,它在維護元素順序的同時,也會保證二叉樹的平衡,即葉節點的深度差最多爲1。
創建一個空的 TreeSet ,可以先定義排序規則:
scala> val myOrdering = Ordering.fromLessThan[String](/DOC_Scala/chinese_scala_offical_document/file/_ > _)
myOrdering: scala.math.Ordering[String] = ...
然後,用這一排序規則創建一個空的樹集:
scala> TreeSet.empty(myOrdering)
res1: scala.collection.immutable.TreeSet[String] = TreeSet()
或者,你也可以不指定排序規則參數,只需要給定一個元素類型或空集合。在這種情況下,將使用此元素類型默認的排序規則。
scala> TreeSet.empty[String]
res2: scala.collection.immutable.TreeSet[String] = TreeSet()
如果通過已有的TreeSet來創建新的集合(例如,通過串聯或過濾操作),這些集合將和原集合保持相同的排序規則。例如,
scala> res2 + ("one", "two", "three", "four")
res3: scala.collection.immutable.TreeSet[String] = TreeSet(four, one, three, two)
有序集合同樣支持元素的範圍操作。例如,range方法返回從指定起始位置到結束位置(不含結束元素)的所有元素,from方法返回大於等於某個元素的所有元素。調用這兩種方法的返回值依然是有序集合。例如:
scala> res3 range ("one", "two")
res4: scala.collection.immutable.TreeSet[String] = TreeSet(one, three)
scala> res3 from "three"
res5: scala.collection.immutable.TreeSet[String] = TreeSet(three, two)
位集合(Bitset)
位集合是由單字或多字的緊湊位實現的非負整數的集合。其內部使用Long型數組來表示。第一個Long元素表示的範圍爲0到63,第二個範圍爲64到127,以此類推(值爲0到127的非可變位集合通過直接將值存儲到第一個或第兩個Long字段的方式,優化掉了數組處理的消耗)。對於每個Long,如果有相應的值包含於集合中則它對應的位設置爲1,否則該位爲0。這裏遵循的規律是,位集合的大小取決於存儲在該集合的最大整數的值的大小。假如N是爲集合所要表示的最大整數,則集合的大小就是N/64個長整形字,或者N/8個字節,再加上少量額外的狀態信息字節。
因此當位集合包含的元素值都比較小時,它比其他的集合類型更緊湊。位集合的另一個優點是它的contains方法(成員測試)、+=運算(添加元素)、-=運算(刪除元素)都非常的高效。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/sets.html
映射(Map)
映射(Map)是一種可迭代的鍵值對結構(也稱映射或關聯)。Scala的Predef類提供了隱式轉換,允許使用另一種語法:key -> value
,來代替(key, value)
。如:Map("x"
-> 24, "y" -> 25, "z" -> 26)
等同於Map(("x", 24), ("y", 25), ("z", 26))
,卻更易於閱讀。
映射(Map)的基本操作與集合(Set)類似。下面的表格分類總結了這些操作:
- 查詢類操作:apply、get、getOrElse、contains和DefinedAt。它們都是根據主鍵獲取對應的值映射操作。例如:def get(key): Option[Value]。“m get key” 返回m中是否用包含了key值。如果包含了,則返回對應value的Some類型值。否則,返回None。這些映射中也包括了apply方法,該方法直接返回主鍵對應的值。apply方法不會對值進行Option封裝。如果該主鍵不存在,則會拋出異常。
- 添加及更新類操作:+、++、updated,這些映射操作允許你添加一個新的綁定或更改現有的綁定。
- 刪除類操作:-、--,從一個映射(Map)中移除一個綁定。
- 子集類操作:keys、keySet、keysIterator、values、valuesIterator,可以以不同形式返回映射的鍵和值。
- filterKeys、mapValues等變換用於對現有映射中的綁定進行過濾和變換,進而生成新的映射。
Map類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
查詢: | |
ms get k | 返回一個Option,其中包含和鍵k關聯的值。若k不存在,則返回None。 |
ms(k) | (完整寫法是ms apply k)返回和鍵k關聯的值。若k不存在,則拋出異常。 |
ms getOrElse (k, d) | 返回和鍵k關聯的值。若k不存在,則返回默認值d。 |
ms contains k | 檢查ms是否包含與鍵k相關聯的映射。 |
ms isDefinedAt k | 同contains。 |
添加及更新: | |
ms + (k -> v) | 返回一個同時包含ms中所有鍵值對及從k到v的鍵值對k -> v的新映射。 |
ms + (k -> v, l -> w) | 返回一個同時包含ms中所有鍵值對及所有給定的鍵值對的新映射。 |
ms ++ kvs | 返回一個同時包含ms中所有鍵值對及kvs中的所有鍵值對的新映射。 |
ms updated (k, v) | 同ms + (k -> v)。 |
移除: | |
ms - k | 返回一個包含ms中除鍵k以外的所有映射關係的映射。 |
ms - (k, 1, m) | 返回一個濾除了ms中與所有給定的鍵相關聯的映射關係的新映射。 |
ms -- ks | 返回一個濾除了ms中與ks中給出的鍵相關聯的映射關係的新映射。 |
子容器(Subcollection): | |
ms.keys | 返回一個用於包含ms中所有鍵的iterable對象(譯註:請注意iterable對象與iterator的區別) |
ms.keySet | 返回一個包含ms中所有的鍵的集合。 |
ms.keyIterator | 返回一個用於遍歷ms中所有鍵的迭代器。 |
ms.values | 返回一個包含ms中所有值的iterable對象。 |
ms.valuesIterator | 返回一個用於遍歷ms中所有值的迭代器。 |
變換: | |
ms filterKeys p | 一個映射視圖(Map View),其包含一些ms中的映射,且這些映射的鍵滿足條件p。用條件謂詞p過濾ms中所有的鍵,返回一個僅包含與過濾出的鍵值對的映射視圖(view)。 |
ms mapValues f | 用f將ms中每一個鍵值對的值轉換成一個新的值,進而返回一個包含所有新鍵值對的映射視圖(view)。 |
可變映射(Map)還支持下表中列出的操作。
mutable.Map類中的操作
WHAT IT IS | WHAT IT DOES |
---|---|
添加及更新 | |
ms(k) = v | (完整形式爲ms.update(x, v))。向映射ms中新增一個以k爲鍵、以v爲值的映射關係,ms先前包含的以k爲值的映射關係將被覆蓋。 |
ms += (k -> v) | 向映射ms增加一個以k爲鍵、以v爲值的映射關係,並返回ms自身。 |
ms += (k -> v, l -> w) | 向映射ms中增加給定的多個映射關係,並返回ms自身。 |
ms ++= kvs | 向映射ms增加kvs中的所有映射關係,並返回ms自身。 |
ms put (k, v) | 向映射ms增加一個以k爲鍵、以v爲值的映射,並返回一個Option,其中可能包含此前與k相關聯的值。 |
ms getOrElseUpdate (k, d) | 如果ms中存在鍵k,則返回鍵k的值。否則向ms中新增映射關係k -> v並返回d。 |
移除: | |
ms -= k | 從映射ms中刪除以k爲鍵的映射關係,並返回ms自身。 |
ms -= (k, l, m) | 從映射ms中刪除與給定的各個鍵相關聯的映射關係,並返回ms自身。 |
ms --= ks | 從映射ms中刪除與ks給定的各個鍵相關聯的映射關係,並返回ms自身。 |
ms remove k | 從ms中移除以k爲鍵的映射關係,並返回一個Option,其可能包含之前與k相關聯的值。 |
ms retain p | 僅保留ms中鍵滿足條件謂詞p的映射關係。 |
ms.clear() | 刪除ms中的所有映射關係 |
變換: | |
ms transform f | 以函數f轉換ms中所有鍵值對(譯註:原文比較含糊,transform中參數f的類型是(A, B) => B,即對ms中的所有鍵值對調用f,得到一個新的值,並用該值替換原鍵值對中的值)。 |
克隆: | |
ms.clone | 返回一個新的可變映射(Map),其中包含與ms相同的映射關係。 |
映射(Map)的添加和刪除操作與集合(Set)的相關操作相同。同集合(Set)操作一樣,可變映射(mutable maps)也支持非破壞性(non-destructive)修改操作+、-、和updated。但是這些操作涉及到可變映射的複製,因此較少被使用。而利用兩種變形m(key) = value和m +=
(key -> value)
, 我們可以“原地”修改可變映射m。此外,存還有一種變形m put (key, value)
,該調用返回一個Option值,其中包含此前與鍵相關聯的值,如果不存在這樣的值,則返回None。
getOrElseUpdate特別適合用於訪問用作緩存的映射(Map)。假設調用函數f開銷巨大:
scala> def f(x: String) = {
println("taking my time."); sleep(100)
x.reverse }
f: (x: String)String
此外,再假設f沒有副作用,即反覆以相同參數調用f,得到的結果都相同。那麼,我們就可以將之前的調用參數和計算結果保存在一個映射(Map)內,今後僅在映射中查不到對應參數的情況下實際調用f,這樣就可以節約時間。這個映射便可以認爲是函數f的緩存。
val cache = collection.mutable.Map[String, String]()
cache: scala.collection.mutable.Map[String,String] = Map()
現在,我們可以寫出一個更高效的帶緩存的函數f:
scala> def cachedF(s: String) = cache.getOrElseUpdate(s, f(s))
cachedF: (s: String)String
scala> cachedF("abc")
稍等片刻。
res3: String = cba
scala> cachedF("abc")
res4: String = cba
注意,getOrElseUpdate的第2個參數是“按名稱(by-name)"傳遞的,所以,僅當在緩存映射中找不到第1個參數,而getOrElseUpdate需要其第2個參數的值時,上述的f("abc")纔會被執行。當然我們也可以利用Map的基本操作直接實現cachedF,但那樣寫就要冗長很多了。
def cachedF(arg: String) = cache get arg match {
case Some(result) => result
case None =>
val result = f(x)
cache(arg) = result
result
}
同步集合(Set)和映射(Map)
無論什麼樣的Map實現,只需混入SychronizedMap trait
,就可以得到對應的線程安全版的Map。例如,我們可以像下述代碼那樣在HashMap中混入SynchronizedMap。這個示例一上來先從scala.colletion.mutable
包中import了兩個trait:Map、SynchronizedMap,和一個類:HashMap。接下來,示例中定義了一個單例對象MapMaker,其中定義了一個方法makeMap。該方法的返回值類型是一個同時以String爲鍵值類型的可變映射。
import scala.collection.mutable.{Map,
SynchronizedMap, HashMap}
object MapMaker {
def makeMap: Map[String, String] = {
new HashMap[String, String] with
SynchronizedMap[String, String] {
override def default(key: String) =
"Why do you want to know?"
}
}
}
混入SynchronizedMap trait
makeMap方法中的第1個語句構造了一個新的混入了SynchronizedMap trait的可變映射:
new HashMap[String, String] with
SynchronizedMap[String, String]
針對這段代碼,Scala編譯器會合成HashMap的一個混入了SynchronizedMap trait的子類,同時生成(並返回)該合成子類的一個實例。處於下面這段代碼的緣故,這個合成類還覆寫了default方法:
override def default(key: String) =
"Why do you want to know?"
當向某個Map查詢給定的鍵所對應的值,而Map中不存在與該鍵相關聯的值時,默認情況下會觸發一個NoSuchElementException異常。不過,如果自定義一個Map類並覆寫default方法,便可以針對不存在的鍵返回一個default方法返回的值。所以,編譯器根據上述代碼合成的HashMap子類在碰到不存在的鍵時將會反過來質問你“Why do you want to know?”
makeMap方法返回的可變映射混入了 SynchronizedMap trait,因此可以用在多線程環境下。對該映射的每次訪問都是同步的。以下示例展示的是從解釋器內以單個線程訪問該映射:
scala> val capital = MapMaker.makeMap
capital: scala.collection.mutable.Map[String,String] = Map()
scala> capital ++ List("US" -> "Washington",
"Paris" -> "France", "Japan" -> "Tokyo")
res0: scala.collection.mutable.Map[String,String] =
Map(Paris -> France, US -> Washington, Japan -> Tokyo)
scala> capital("Japan")
res1: String = Tokyo
scala> capital("New Zealand")
res2: String = Why do you want to know?
scala> capital += ("New Zealand" -> "Wellington")
scala> capital("New Zealand")
res3: String = Wellington
同步集合(synchronized set)的創建方法與同步映射(synchronized map)類似。例如,我們可以通過混入SynchronizedSet trait來創建同步哈希集:
import scala.collection.mutable //導入包scala.collection.mutable
val synchroSet =
new mutable.HashSet[Int] with
mutable.SynchronizedSet[Int]
最後,如有使用同步容器(synchronized collection)的需求,還可以考慮使用java.util.concurrent
中提供的併發容器(concurrent collections)。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/maps.html。
不可變集實體類(Concrete Immutable Collection Classes)
Scala中提供了多種具體的不可變集類供你選擇,這些類(maps, sets, sequences)實現的接口(traits)不同,比如是否能夠是無限(infinite)的,各種操作的速度也不一樣。下面的篇幅介紹幾種Scala中最常用的不可變集類型。
List(列表)
列表List是一種有限的不可變序列式。提供了常數時間的訪問列表頭元素和列表尾的操作,並且提供了常熟時間的構造新鏈表的操作,該操作將一個新的元素插入到列表的頭部。其他許多操作則和列表的長度成線性關係。
List通常被認爲是Scala中最重要的數據結構,所以我們在此不必過於贅述。版本2.8中主要的變化是,List類和其子類::以及其子對象Nil都被定義在了其邏輯上所屬的scala.collection.immutable包裏。scala包中仍然保留了List,Nil和::的別名,所以對於用戶來說可以像原來一樣訪問List。
另一個主要的變化是,List現在更加緊密的融入了Collections Framework中,而不是像過去那樣更像一個特例。比如說,大量原本存在於與List相關的對象的方法基本上全部都過時(deprecated)了,取而代之的是被每種Collection所繼承的統一的構造方法。
Stream(流)
流Stream與List很相似,只不過其中的每一個元素都經過了一些簡單的計算處理。也正是因爲如此,stream結構可以無限長。只有那些被要求的元素纔會經過計算處理,除此以外stream結構的性能特性與List基本相同。
鑑於List通常使用 ::
運算符來進行構造,stream使用外觀上很相像的#::
。這裏用一個包含整數1,2和3的stream來做一個簡單的例子:
scala> val str = 1 #:: 2 #:: 3 #:: Stream.empty
str: scala.collection.immutable.Stream[Int] = Stream(1, ?)
該stream的頭結點是1,尾是2和3.尾部並沒有被打印出來,因爲還沒有被計算。stream被特別定義爲懶惰計算,並且stream的toString方法很謹慎的設計爲不去做任何額外的計算。
下面給出一個稍複雜些的例子。這裏講一個以兩個給定的數字爲起始的斐波那契數列轉換成stream。斐波那契數列的定義是,序列中的每個元素等於序列中在它之前的兩個元素之和。
scala> def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b)
fibFrom: (a: Int,b: Int)Stream[Int]
這個函數看起來比較簡單。序列中的第一個元素顯然是a,其餘部分是以b和位於其後的a+b爲開始斐波那契數列。這段程序最大的亮點是在對序列進行計算的時候避免了無限遞歸。如果函數中使用::
來替換#::
,那麼之後的每次調用都會產生另一次新的調用,從而導致無限遞歸。在此例中,由於使用了#::
,等式右值中的調用在需要求值之前都不會被展開。這裏嘗試着打印出以1,1開頭的斐波那契數列的前幾個元素:
scala> val fibs = fibFrom(1, 1).take(7)
fibs: scala.collection.immutable.Stream[Int] = Stream(1, ?)
scala> fibs.toList
res9: List[Int] = List(1, 1, 2, 3, 5, 8, 13)
Vector(向量)
對於只需要處理數據結構頭結點的算法來說,List非常高效。可是相對於訪問、添加和刪除List頭結點只需要固定時間,訪問和修改頭結點之後元素所需要的時間則是與List深度線性相關的。
向量Vector是用來解決列表(list)不能高效的隨機訪問的一種結構。Vector結構能夠在“更高效”的固定時間內訪問到列表中的任意元素。雖然這個時間會比訪問頭結點或者訪問某數組元素所需的時間長一些,但至少這個時間也是個常量。因此,使用Vector的算法不必僅是小心的處理數據結構的頭結點。由於可以快速修改和訪問任意位置的元素,所以對Vector結構做寫操作很方便。
Vector類型的構建和修改與其他的序列結構基本一樣。
scala> val vec = scala.collection.immutable.Vector.empty
vec: scala.collection.immutable.Vector[Nothing] = Vector()
scala> val vec2 = vec :+ 1 :+ 2
vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2)
scala> val vec3 = 100 +: vec2
vec3: scala.collection.immutable.Vector[Int] = Vector(100, 1, 2)
scala> vec3(0)
res1: Int = 100
Vector結構通常被表示成具有高分支因子的樹(樹或者圖的分支因子是指數據結構中每個節點的子節點數目)。每一個樹節點包含最多32個vector元素或者至多32個子樹節點。包含最多32個元素的vector可以表示爲一個單一節點,而一個間接引用則可以用來表示一個包含至多32*32=1024
個元素的vector。從樹的根節點經過兩跳到達葉節點足夠存下有2的15次方個元素的vector結構,經過3跳可以存2的20次方個,4跳2的25次方個,5跳2的30次方個。所以對於一般大小的vector數據結構,一般經過至多5次數組訪問就可以訪問到指定的元素。這也就是我們之前所提及的隨機數據訪問時“運行時間的相對高效”。
由於Vectors結構是不可變的,所以您不能通過修改vector中元素的方法來返回一個新的vector。儘管如此,您仍可以通過update方法從一個單獨的元素中創建出區別於給定數據結構的新vector結構:
scala> val vec = Vector(1, 2, 3)
vec: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)
scala> vec updated (2, 4)
res0: scala.collection.immutable.Vector[Int] = Vector(1, 2, 4)
scala> vec
res1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)
從上面例子的最後一行我們可以看出,update方法的調用並不會改變vec的原始值。與元素訪問類似,vector的update方法的運行時間也是“相對高效的固定時間”。對vector中的某一元素進行update操作可以通過從樹的根節點開始拷貝該節點以及每一個指向該節點的節點中的元素來實現。這就意味着一次update操作能夠創建1到5個包含至多32個元素或者子樹的樹節點。當然,這樣做會比就地更新一個可變數組敗家很多,但比起拷貝整個vector結構還是綠色環保了不少。
由於vector在快速隨機選擇和快速隨機更新的性能方面做到很好的平衡,所以它目前正被用作不可變索引序列的默認實現方式。
scala> collection.immutable.IndexedSeq(1, 2, 3)
res2: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)
Immutable stacks(不可變棧)
如果您想要實現一個後入先出的序列,那您可以使用Stack。您可以使用push向棧中壓入一個元素,用pop從棧中彈出一個元素,用top查看棧頂元素而不用刪除它。所有的這些操作都僅僅耗費固定的運行時間。
這裏提供幾個簡單的stack操作的例子:
scala> val stack = scala.collection.immutable.Stack.empty
stack: scala.collection.immutable.Stack[Nothing] = Stack()
scala> val hasOne = stack.push(1)
hasOne: scala.collection.immutable.Stack[Int] = Stack(1)
scala> stack
stack: scala.collection.immutable.Stack[Nothing] = Stack()
scala> hasOne.top
res20: Int = 1
scala> hasOne.pop
res19: scala.collection.immutable.Stack[Int] = Stack()
不可變stack一般很少用在Scala編程中,因爲List結構已經能夠覆蓋到它的功能:push操作同List中的::基本相同,pop則對應着tail。
Immutable Queues(不可變隊列)
Queue是一種與stack很相似的數據結構,除了與stack的後入先出不同,Queue結構的是先入先出的。
這裏給出一個創建空不可變queue的例子:
scala> val empty = scala.collection.immutable.Queue[Int]()
empty: scala.collection.immutable.Queue[Int] = Queue()
您可以使用enqueue方法在不可變Queue中加入一個元素:
scala> val has1 = empty.enqueue(1)
has1: scala.collection.immutable.Queue[Int] = Queue(1)
如果想要在queue中添加多個元素需要在調用enqueue方法時用一個collection對象作爲參數:
scala> val has123 = has1.enqueue(List(2, 3))
has123: scala.collection.immutable.Queue[Int]
= Queue(1, 2, 3)
如果想要從queue的頭部刪除一個元素,您可以使用dequeue方法:
scala> val (element, has23) = has123.dequeue
element: Int = 1
has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)
請注意,dequeue方法將會返回兩個值,包括被刪除掉的元素和queue中剩下的部分。
Ranges (等差數列)
[Range]表示的是一個有序的等差整數數列。比如說,“1,2,3,”就是一個Range,“5,8,11,14,”也是。在Scala中創建一個Range類,需要用到兩個預定義的方法to和by。
scala> 1 to 3
res2: scala.collection.immutable.Range.Inclusive
with scala.collection.immutable.Range.ByOne = Range(1, 2, 3)
scala> 5 to 14 by 3
res3: scala.collection.immutable.Range = Range(5, 8, 11, 14)
如果您想創建一個不包含範圍上限的Range類,那麼用until方法代替to更爲方便:
scala> 1 until 3
res2: scala.collection.immutable.Range.Inclusive
with scala.collection.immutable.Range.ByOne = Range(1, 2)
Range類的空間複雜度是恆定的,因爲只需要三個數字就可以定義一個Range類:起始、結束和步長值。也正是因爲有這樣的特性,對Range類多數操作都非常非常的快。
Hash Tries
Hash try是高效實現不可變集合和關聯數組(maps)的標準方法,immutable.HashMap類提供了對Hash Try的支持。從表現形式上看,Hash Try和Vector比較相似,都是樹結構,且每個節點包含32個元素或32個子樹,差別只是用不同的hash code替換了指向各個節點的向量值。舉個例子吧:當您要在一個映射表裏找一個關鍵字,首先需要用hash code值替換掉之前的向量值;然後用hash code的最後5個bit找到第一層子樹,然後每5個bit找到下一層子樹。當存儲在一個節點中所有元素的代表他們當前所在層的hash code位都不相同時,查找結束。
Hash Try對於快速查找和函數式的高效添加和刪除操作上取得了很好的平衡,這也是Scala中不可變映射和集合採用Hash Try作爲默認實現方式的原因。事實上,Scala對於大小小於5的不可變集合和映射做了更進一步的優化。只有1到4個元素的集合和映射被在現場會被存儲在一個單獨僅僅包含這些元素(對於映射則只是包含鍵值對)的對象中。空集合和空映射則視情況不同作爲一個單獨的對象,空的一般情況下就會一直空下去,所以也沒有必要爲他們複製一份拷貝。
Red-Black Trees(紅黑樹)
紅黑樹是一種平衡二叉樹,樹中一些節點被設計成紅節點,其餘的作爲黑節點。同任何平衡二叉樹一樣,對紅黑樹的最長運行時間隨樹的節點數成對數(logarithmic)增長。
Scala隱含的提供了不可變集合和映射的紅黑樹實現,您可以在TreeSet和TreeMap下使用這些方法。
## scala> scala.collection.immutable.TreeSet.empty[Int]
res11: scala.collection.immutable.TreeSet[Int] = TreeSet()
scala> res11 + 1 + 3 + 3
res12: scala.collection.immutable.TreeSet[Int] = TreeSet(1, 3)
紅黑樹在Scala中被作爲SortedSet的標準實現,因爲它提供了一個高效的迭代器,可以用來按照拍好的序列返回所有的元素。
Immutable BitSets(不可變位集合)
BitSet代表一個由小整數構成的容器,這些小整數的值表示了一個大整數被置1的各個位。比如說,一個包含3、2和0的bit集合可以用來表示二進制數1101和十進制數13.
BitSet內部的使用了一個64位long型的數組。數組中的第一個long表示整數0到63,第二個表示64到27,以此類推。所以只要集合中最大的整數在千以內BitSet的壓縮率都是相當高的。
BitSet操作的運行時間是非常快的。查找測試僅僅需要固定時間。向集合內增加一個項所需時間同BitSet數組中long型的個數成正比,但這也通常是個非常小的值。這裏有幾個關於BitSet用法的例子:
scala> val bits = scala.collection.immutable.BitSet.empty
bits: scala.collection.immutable.BitSet = BitSet()
scala> val moreBits = bits + 3 + 4 + 4
moreBits: scala.collection.immutable.BitSet = BitSet(3, 4)
scala> moreBits(3)
res26: Boolean = true
scala> moreBits(0)
res27: Boolean = false
List Maps
ListMap被用來表示一個保存鍵-值映射的鏈表。一般情況下,ListMap操作都需要遍歷整個列表,所以操作的運行時間也同列表長度成線性關係。實際上ListMap在Scala中很少使用,因爲標準的不可變映射通常速度會更快。唯一的例外是,在構造映射時由於某種原因,鏈表中靠前的元素被訪問的頻率大大高於其他的元素。
scala> val map = scala.collection.immutable.ListMap(1->"one", 2->"two")
map: scala.collection.immutable.ListMap[Int,java.lang.String] =
Map(1 -> one, 2 -> two)
scala> map(2)
res30: String = "two"
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/concrete-immutable-collection-classes.html
具體的可變容器類(Concrete Mutable Collection Classes)
目前你已經看過了Scala的不可變容器類,這些是標準庫中最常用的。現在來看一下可變容器類。
Array Buffers
一個ArrayBuffer緩衝包含數組和數組的大小。對數組緩衝的大多數操作,其速度與數組本身無異。因爲這些操作直接訪問、修改底層數組。另外,數組緩衝可以進行高效的尾插數據。追加操作均攤下來只需常量時間。因此,數組緩衝可以高效的建立一個有大量數據的容器,無論是否總有數據追加到尾部。
scala> val buf = scala.collection.mutable.ArrayBuffer.empty[Int]
buf: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()
scala> buf += 1
res32: buf.type = ArrayBuffer(1)
scala> buf += 10
res33: buf.type = ArrayBuffer(1, 10)
scala> buf.toArray
res34: Array[Int] = Array(1, 10)
List Buffers
ListBuffer 類似於數組緩衝。區別在於前者內部實現是鏈表, 而非數組。如果你想把構造完的緩衝轉換爲列表,那就用列表緩衝,別用數組緩衝。
scala> val buf = scala.collection.mutable.ListBuffer.empty[Int]
buf: scala.collection.mutable.ListBuffer[Int] = ListBuffer()
scala> buf += 1
res35: buf.type = ListBuffer(1)
scala> buf += 10
res36: buf.type = ListBuffer(1, 10)
scala> buf.toList
res37: List[Int] = List(1, 10)
StringBuilders
數組緩衝用來構建數組,列表緩衝用來創建列表。類似地,StringBuilder 用來構造字符串。作爲常用的類,字符串構造器已導入到默認的命名空間。直接用 new StringBuilder就可創建字符串構造器 ,像這樣:
scala> val buf = new StringBuilder
buf: StringBuilder =
scala> buf += 'a'
res38: buf.type = a
scala> buf ++= "bcdef"
res39: buf.type = abcdef
scala> buf.toString
res41: String = abcdef
鏈表
鏈表是可變序列,它由一個個使用next指針進行鏈接的節點構成。它們的支持類是LinkedList。在大多數的編程語言中,null可以表示一個空鏈表,但是在Scalable集合中不是這樣。因爲就算是空的序列,也必須支持所有的序列方法。尤其是 LinkedList.empty.isEmpty
必須返回true
,而不是拋出一個NullPointerException
。空鏈表用一種特殊的方式編譯:
它們的 next 字段指向它自身。鏈表像他們的不可變對象一樣,是最佳的順序遍歷序列。此外,鏈表可以很容易去插入一個元素或鏈接到另一個鏈表。
雙向鏈表
雙向鏈表和單向鏈表相似,只不過它們除了具有 next字段外,還有一個可變字段 prev用來指向當前節點的上一個元素 。這個多出的鏈接的好處主要在於可以快速的移除元素。雙向鏈表的支持類是DoubleLinkedList.
可變列表
MutableList 由一個單向鏈表和一個指向該鏈表終端空節點的指針構成。因爲避免了貫穿整個列表去遍歷搜索它的終端節點,這就使得列表壓縮了操作所用的時間。MutableList 目前是Scala中mutable.LinearSeq 的標準實現。
隊列
Scala除了提供了不可變隊列之外,還提供了可變隊列。你可以像使用一個不可變隊列一樣地使用一個可變隊列,但你需要使用+= 和++=操作符進行添加的方式來替代排隊方法。 當然,在一個可變隊列中,出隊方法將只移除頭元素並返回該隊列。這裏是一個例子:
scala> val queue = new scala.collection.mutable.Queue[String]
queue: scala.collection.mutable.Queue[String] = Queue()
scala> queue += "a"
res10: queue.type = Queue(a)
scala> queue ++= List("b", "c")
res11: queue.type = Queue(a, b, c)
scala> queue
res12: scala.collection.mutable.Queue[String] = Queue(a, b, c)
scala> queue.dequeue
res13: String = a
scala> queue
res14: scala.collection.mutable.Queue[String] = Queue(b, c)
數組序列
Array Sequences 是具有固定大小的可變序列。在它的內部,用一個 Array[Object]
來存儲元素。在Scala 中,ArraySeq 是它的實現類。
如果你想擁有 Array 的性能特點,又想建立一個泛型序列實例,但是你又不知道其元素的類型,在運行階段也無法提供一個ClassManifest
,那麼你通常可以使用 ArraySeq
。這些問題在arrays一節中有詳細的說明。
堆棧
你已經在前面看過了不可變棧。還有一個可變棧,支持類是mutable.Stack。它的工作方式與不可變棧相同,只是適當的做了修改。
scala> val stack = new scala.collection.mutable.Stack[Int]
stack: scala.collection.mutable.Stack[Int] = Stack()
scala> stack.push(1)
res0: stack.type = Stack(1)
scala> stack
res1: scala.collection.mutable.Stack[Int] = Stack(1)
scala> stack.push(2)
res0: stack.type = Stack(1, 2)
scala> stack
res3: scala.collection.mutable.Stack[Int] = Stack(1, 2)
scala> stack.top
res8: Int = 2
scala> stack
res9: scala.collection.mutable.Stack[Int] = Stack(1, 2)
scala> stack.pop
res10: Int = 2
scala> stack
res11: scala.collection.mutable.Stack[Int] = Stack(1)
數組堆棧
ArrayStack 是另一種可變棧的實現,用一個可根據需要改變大小的數組做爲支持。它提供了快速索引,使其通常在大多數的操作中會比普通的可變堆棧更高效一點。
哈希表
Hash Table 用一個底層數組來存儲元素,每個數據項在數組中的存儲位置由這個數據項的Hash Code 來決定。添加一個元素到Hash Table不用花費多少時間,只要數組中不存在與其含有相同Hash Code的另一個元素。因此,只要Hash Table能夠根據一種良好的hash codes分配機制來存放對象,Hash Table的速度會非常快。所以在Scala中默認的可變map和set都是基於Hash Table的。你也可以直接用mutable.HashSet 和 mutable.HashMap 來訪問它們。
Hash Set 和 Map 的使用和其他的Set和Map是一樣的。這裏有一些簡單的例子:
scala> val map = scala.collection.mutable.HashMap.empty[Int,String]
map: scala.collection.mutable.HashMap[Int,String] = Map()
scala> map += (1 -> "make a web site")
res42: map.type = Map(1 -> make a web site)
scala> map += (3 -> "profit!")
res43: map.type = Map(1 -> make a web site, 3 -> profit!)
scala> map(1)
res44: String = make a web site
scala> map contains 2
res46: Boolean = false
Hash Table的迭代並不是按特定的順序進行的。它是按任何可能的順序,依次處理底層數組的數據。爲了保證迭代的次序,可以使用一個Linked Hash Map 或 Set 來做爲替代。Linked Hash Map 或 Set 像標準的Hash Map 或 Set一樣,只不過它包含了一個Linked List,其中的元素按添加的順序排列。在這種容器中的迭代都是具有相同的順序,就是按照元素最初被添加的順序進行迭代。
Weak Hash Maps
Weak Hash Map 是一種特殊的Hash Map,垃圾回收器會忽略從Map到存儲在其內部的Key值的鏈接。這也就是說,當一個key不再被引用的時候,這個鍵和對應的值會從map中消失。Weak Hash Map 可以用來處理緩存,比如當一個方法被同一個鍵值重新調用時,你想重用這個大開銷的方法返回值。如果Key值和方法返回值存儲在一個常規的Hash Map裏,Map會無限制的擴展,Key值也永遠不會被垃圾回收器回收。用Weak
Hash Map會避免這個問題。一旦有一個Key對象不再被引用,那它的實體會從Weak Hash Map中刪除。在Scala中,WeakHashMap類是Weak
Hash Map的實現類,封裝了底層的Java實現類java.util.WeakHashMap
。
Concurrent Maps
Concurrent Map可以同時被多個線程訪問。除了Map的通用方法,它提供了下列的原子方法:
Concurrent Map類中的方法:
WHAT IT IS | WHAT IT DOES |
---|---|
m putIfAbsent(k, v) | 添加 鍵/值 綁定 k -> m ,如果k在m中沒有被定義過 |
m remove (k, v) | 如果當前 k 映射於 v,刪除k對應的實體。 |
m replace (k, old, new) | 如果k先前綁定的是old,則把鍵k 關聯的值替換爲new。 |
m replace (k, v) | 如果k先前綁定的是其他值,則把鍵k對應的值替換爲v |
ConcurrentMap
體現了Scala容器庫的特性。目前,它的實現類只有Java的java.util.concurrent.ConcurrentMap
, 它可以用standard
Java/Scala collection conversions(標準的java/Scala容器轉換器)來自動轉換成一個Scala map。
Mutable Bitsets
一個類型爲mutable.BitSet的可變bit集合和不可變的bit集合很相似,它只是做了適當的修改。Mutable bit sets在更新的操作上比不可變bit set 效率稍高,因爲它不必複製沒有發生變化的 Long值。
scala> val bits = scala.collection.mutable.BitSet.empty
bits: scala.collection.mutable.BitSet = BitSet()
scala> bits += 1
res49: bits.type = BitSet(1)
scala> bits += 3
res50: bits.type = BitSet(1, 3)
scala> bits
res51: scala.collection.mutable.BitSet = BitSet(1, 3)
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/concrete-mutable-collection-classes.html
數組(Array)
在Scala中,數組是一種特殊的collection。一方面,Scala數組與Java數組是一一對應的。即Scala數組Array[Int]可看作Java的Int[],Array[Double]可看作Java的double[],以及Array[String]可看作Java的String[]。但Scala數組比Java數組提供了更多內容。首先,Scala數組是一種泛型。即可以定義一個Array[T],T可以是一種類型參數或抽象類型。其次,Scala數組與Scala序列是兼容的 - 在需要Seq[T]的地方可由Array[T]代替。最後,Scala數組支持所有的序列操作。這裏有個實際的例子:
scala> val a1 = Array(1, 2, 3)
a1: Array[Int] = Array(1, 2, 3)
scala> val a2 = a1 map (_ * 3)
a2: Array[Int] = Array(3, 6, 9)
scala> val a3 = a2 filter (_ % 2 != 0)
a3: Array[Int] = Array(3, 9)
scala> a3.reverse
res1: Array[Int] = Array(9, 3)
既然Scala數組表現的如同Java的數組,那麼Scala數組這些額外的特性是如何運作的呢?實際上,Scala 2.8與早期版本在這個問題的處理上有所不同。早期版本中執行打包/解包過程時,Scala編譯器做了一些“神奇”的包裝/解包的操作,進行數組與序列對象之間互轉。其中涉及到的細節相當複雜,尤其是創建一個新的泛型類型數組Array[T]時。一些讓人迷惑的罕見實例以及數組操作的性能都是不可預測的。
Scala 2.8設計要簡單得多,其數組實現系統地使用隱式轉換,從而基本去除了編譯器的特殊處理。Scala 2.8中數組不再看作序列,因爲本地數組的類型不是Seq的子類型。而是在數組和 scala.collection.mutable.WrappedArray
這個類的實例之間隱式轉換,後者則是Seq的子類。這裏有個例子:
scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(1, 2, 3)
scala> val a4: Array[Int] = s.toArray
a4: Array[Int] = Array(1, 2, 3)
scala> a1 eq a4
res2: Boolean = true
上面的例子說明數組與序列是兼容的,因爲數組可以隱式轉換爲WrappedArray。反之可以使用Traversable提供的toArray方法將WrappedArray轉換爲數組。REPL最後一行表明,隱式轉換與toArray方法作用相互抵消。
數組還有另外一種隱式轉換,不需要將數組轉換成序列,而是簡單地把所有序列的方法“添加”給數組。“添加”其實是將數組封裝到一個ArrayOps類型的對象中,後者支持所有序列的方法。ArrayOps對象的生命週期通常很短暫,不調用序列方法的時候基本不會用到,其內存也可以回收。現代虛擬機一般不會創建這個對象。
在接下來REPL中展示數組的這兩種隱式轉換的區別:
scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(1, 2, 3)
scala> seq.reverse
res2: Seq[Int] = WrappedArray(3, 2, 1)
scala> val ops: collection.mutable.ArrayOps[Int] = a1
ops: scala.collection.mutable.ArrayOps[Int] = [I(1, 2, 3)
scala> ops.reverse
res3: Array[Int] = Array(3, 2, 1)
注意seq是一個WrappedArray,seq調用reverse方法也會得到一個WrappedArray。這是沒問題的,因爲封裝的數組就是Seq,在任意Seq上調用reverse方法都會得到Seq。反之,變量ops屬於ArrayOps這個類,對其調用reverse方法得到一個數組,而不是Seq。
上例直接使用ArrayOps僅爲了展示其與WrappedArray的區別,這種用法非常不自然。一般情況下永遠不要實例化一個ArrayOps,而是在數組上調用Seq的方法:
scala> a1.reverse
res4: Array[Int] = Array(3, 2, 1)
ArrayOps的對象會通過隱式轉換自動的插入,因此上述的代碼等價於
scala> intArrayOps(a1).reverse
res5: Array[Int] = Array(3, 2, 1)
這裏的intArrayOps就是之前例子中插入的隱式轉換。這裏引出一個疑問,上面代碼中,編譯器爲何選擇了intArrayOps而不是WrappedArray做隱式轉換?畢竟,兩種轉換都是將數組映射到支持reverse方法的類型,並且指定輸入。答案是兩種轉換是有優先級次序的,ArrayOps轉換比WrappedArray有更高的優先級。前者定義在Predef對象中,而後者定義在繼承自Predef的scala.LowPritoryImplicits
類中。子類、子對象中隱式轉換的優先級低於基類。所以如果兩種轉換都可用,Predef中的會優先選取。字符串的情況也是如此。
數組與序列兼容,並支持所有序列操作的方法,你現在應該已經瞭然於胸。那泛型呢?在Java中你不可以定義一個以T爲類型參數的T[]
。那麼Scala的Array[T]
是如何做的呢?事實上一個像Array[T]
的泛型數組在運行時態可任意爲Java的八個原始數組類型像byte[]
, short[]
, char[]
,int[]
, long[]
, float[]
, double[]
, boolean[]
,甚至它可以是一個對象數組。最常見的運行時態類型是AnyRef
,它包括了所有的這些類型(相當於java.lang.Object),因此這樣的類型可以通過Scala編譯器映射到Array[T]
.在運行時,當Array[T]
類型的數組元素被訪問或更新時,就會有一個序列的類型測試用於確定真正的數組類型,隨後就是java中的正確的數組操作。這些類型測試會影響數組操作的效率。這意味着如果你需要更大的性能,你應該更喜歡具體而明確的泛型數組。代表通用的泛型數組是不夠的,因此,也必然有一種方式去創造泛型數組。這是一個更難的問題,需要一點點的幫助你。爲了說明這個問題,考慮下面用一個通用的方法去創造數組的嘗試。
//這是錯的!
def evenElems[T](xs: Vector[T]): Array[T] = {
val arr = new Array[T]((xs.length + 1) / 2)
for (i <- 0 until xs.length by 2)
arr(i / 2) = xs(i)
arr
}
evenElems方法返回一個新數組,該數組包含了參數向量xs的所有元素甚至在向量中的位置。evenElems 主體的第一行構建了結果數組,將相同元素類型作爲參數。所以根據T的實際類型參數,這可能是一個Array[Int]
,或者是一個Array[Boolean]
,或者是一個在java中有一些其他基本類型的數組,或者是一個有引用類型的數組。但是這些類型有不同的運行時表達,那麼Scala如何在運行時選擇正確的呢?事實上,它不是基於信息傳遞做的,因爲與類型參數T相對應的實際類型在運行時已被抹去。這就是爲什麼你在編譯上面的代碼時會出現如下的錯誤信息:
error: cannot find class manifest for element type T
val arr = new Array[T]((arr.length + 1) / 2)
^
這裏需要你做的就是通過提供一些運行時的實際元素類型參數的線索來幫助編譯器處理。這個運行時的提示採取的形式是一個scala.reflect.ClassManifest
類型的類聲明。一個類聲明就是一個類型描述對象,給對象描述了一個類型的頂層類。另外,類聲明也有scala.reflect.Manifest
類型的所有聲明,它描述了類型的各個方面。但對於數組創建而言,只需要提供類聲明。
如果你指示編譯器那麼做它就會自動的構建類聲明。“指示”意味着你決定一個類聲明作爲隱式參數,像這樣:
def evenElems[T](xs: Vector[T])(implicit m: ClassManifest[T]): Array[T] = ...
使用一個替換和較短的語法。通過用一個上下文綁定你也可以要求類型與一個類聲明一起。這種方式是跟在一個冒號類型和類名爲ClassManifest的後面,想這樣:
// this works
def evenElems[T: ClassManifest](xs: Vector[T]): Array[T] = {
val arr = new Array[T]((xs.length + 1) / 2)
for (i <- 0 until xs.length by 2)
arr(i / 2) = xs(i)
arr
}
這兩個evenElems的修訂版本意思是完全相同的。當Array[T] 構造時,在任何情況下會發生的是,編譯器會尋找類型參數T的一個類聲明,這就是說,它會尋找ClassManifest[T]一個隱式類型的值。如果如此的一個值被發現,聲明會用來構造正確的數組類型。否則,你就會看到一個錯誤信息像上面一樣。
下面是一些使用evenElems 方法的REPL 交互。
scala> evenElems(Vector(1, 2, 3, 4, 5))
res6: Array[Int] = Array(1, 3, 5)
scala> evenElems(Vector("this", "is", "a", "test", "run"))
res7: Array[java.lang.String] = Array(this, a, run)
在這兩種情況下,Scala編譯器自動的爲元素類型構建一個類聲明(首先,Int,然後String)並且通過它傳遞evenElems 方法的隱式參數。編譯器可以對所有的具體類型構造,但如果論點本身是另一個沒有類聲明的類型參數就不可以。例如,下面的錯誤:
scala> def wrap[U](xs: Array[U]) = evenElems(xs)
<console>:6: error: could not find implicit value for
證明類型ClassManifest[U]的參數
def wrap[U](xs: Array[U]) = evenElems(xs)
^
這裏所發生的是,evenElems 需要一個類型參數U的類聲明,但是沒有發現。這種情況下的解決方案是,當然,是爲了U的另一個隱式類聲明。所以下面起作用了:
scala> def wrap[U: ClassManifest](xs: Array[U]) = evenElems(xs)
wrap: [U](xs: Array[U])(implicit evidence$1: ClassManifest[U])Array[U]
這個實例還顯示在定義U的上下文綁定裏這僅是一個簡短的隱式參數命名爲ClassManifest[U]
類型的evidence$1
。
總結,泛型數組創建需要類聲明。所以每當創建一個類型參數T的數組,你還需要提供一個T的隱式類聲明。最簡單的方法是聲明類型參數與ClassManifest的上下文綁定,如 [T: ClassManifest]
。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/arrays.html
Strings(字符串)
像數組,字符串不是直接的序列,但是他們可以轉換爲序列,並且他們也支持所有的在字符串上的序列操作這裏有些例子讓你可以理解在字符串上操作。
scala> val str = "hello"
str: java.lang.String = hello
scala> str.reverse
res6: String = olleh
scala> str.map(_.toUpper)
res7: String = HELLO
scala> str drop 3
res8: String = lo
scala> str slice (1, 4)
res9: String = ell
scala> val s: Seq[Char] = str
s: Seq[Char] = WrappedString(h, e, l, l, o)
這些操作依賴於兩種隱式轉換。第一種,低優先級轉換映射一個String到WrappedString,它是immutable.IndexedSeq
的子類。在上述代碼中這種轉換應用在一個string轉換爲一個Seq。另一種,高優先級轉換映射一個string到StringOps 對象,從而在immutable
序列到strings上增加了所有的方法。在上面的例子裏,這種隱式轉換插入在reverse,map,drop和slice的方法調用中。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/strings.html
性能特點(Performance Characteristics)
前面的解釋明確說明了不同的容器類型具有不同的性能特點。這通常是選擇容器類型的首要依據。以下的兩張表格,總結了一些關於容器類型常用操作的性能特點。
序列類型的性能特點
head | tail | apply | update | prepend | append | insert |
---|---|---|---|---|---|---|
不可變序列 | ||||||
List | C | C | L | L | C | L |
Stream | C | C | L | L | C | L |
Vector | eC | eC | eC | eC | eC | eC |
Stack | C | C | L | L | C | C |
Queue | aC | aC | L | L | L | C |
Range | C | C | C | - | - | - |
String | C | L | C | L | L | L |
可變序列 | ||||||
ArrayBuffer | C | L | C | C | L | aC |
ListBuffer | C | L | L | L | C | C |
StringBuilder | C | L | C | C | L | aC |
MutableList | C | L | L | L | C | C |
Queue | C | L | L | L | C | C |
ArraySeq | C | L | C | C | - | - |
Stack | C | L | L | L | C | L |
ArrayStack | C | L | C | C | aC | L |
Array | C | L | C | C | - | - |
集合和映射類型的性能特點
lookup | add | remove | min |
---|---|---|---|
不可變序列 | |||
HashSet/HashMap | eC | eC | eC |
TreeSet/TreeMap | Log | Log | Log |
BitSet | C | L | L |
ListMap | L | L | L |
可變序列 | |||
HashSet/HashMap | eC | eC | eC |
WeakHashMap | eC | eC | eC |
BitSet | C | aC | C |
TreeSet | Log | Log | Log |
標註:1 假設位是密集分佈的
這兩個表中的條目:
解釋如下 | |
---|---|
C | 指操作的時間複雜度爲常數 |
eC | 指操作的時間複雜度實際上爲常數,但可能依賴於諸如一個向量最大長度或是哈希鍵的分佈情況等一些假設。 |
aC | 該操作的均攤運行時間爲常數。某些調用的可能耗時較長,但多次調用之下,每次調用的平均耗時是常數。 |
Log | 操作的耗時與容器大小的對數成正比。 |
L | 操作是線性的,耗時與容器的大小成正比。 |
- | 操作不被支持。 |
第一張表處理序列類型——無論可變還是不可變——:
使用以下操作 | |
---|---|
head | 選擇序列的第一個元素。 |
tail | 生成一個包含除第一個元素以外所有其他元素的新的列表。 |
apply | 索引。 |
update | 功能性更新不可變序列,同步更新可變序列。 |
prepend | 添加一個元素到序列頭。對於不可變序列,操作會生成個新的序列。對於可變序列,操作會修改原有的序列。 |
append | 在序列尾部插入一個元素。對於不可變序列,這將產生一個新的序列。對於可變序列,這將修改原有的序列。 |
insert | 在序列的任意位置上插入一個元素。只有可變序列支持該操作。 |
第二個表處理可變和不可變集與映射
使用以下操作: | |
---|---|
lookup | 測試一個元素是否被包含在集合中,或者找出一個鍵對應的值 |
add | 添加一個新的元素到一個集合中或者添加一個鍵值對到一個映射中。 |
remove | 移除一個集合中的一個元素或者移除一個映射中一個鍵。 |
min | 集合中的最小元素,或者映射中的最小鍵。 |
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/performance-characteristics.html
等價性(Equality)
容器庫有標準的等價性和散列法。首先,這個想法是爲了將容器劃分爲集合,序列。不同範疇的容器總是不相等的。例如,即使包含相同的元素,Set(1, 2, 3)
與 List(1, 2, 3)
不等價。另一方面,在同一範疇下的容器是相等的,當且僅當它們具有相同的元素(對於序列:元素要相同,順序要相同)。例如List(1,
2, 3) == Vector(1, 2, 3)
, HashSet(1, 2) == TreeSet(2, 1)
。
一個容器可變與否對等價性校驗沒有任何影響。對於一個可變容器,在執行等價性測試的同時,你可以簡單地思考下它的當前元素。意思是,一個可變容器可能在不同時間等價於不同容器,這是由增加或移除了哪些元素所決定的。當你使用可變容器作爲一個hashmap的鍵時,這將是一個潛在的陷阱。例如:
scala> import collection.mutable.{HashMap, ArrayBuffer}
import collection.mutable.{HashMap, ArrayBuffer}
scala> val buf = ArrayBuffer(1, 2, 3)
buf: scala.collection.mutable.ArrayBuffer[Int] =
ArrayBuffer(1, 2, 3)
scala> val map = HashMap(buf -> 3)
map: scala.collection.mutable.HashMap[scala.collection。
mutable.ArrayBuffer[Int],Int] = Map((ArrayBuffer(1, 2, 3),3))
scala> map(buf)
res13: Int = 3
scala> buf(0) += 1
scala> map(buf)
java.util.NoSuchElementException: key not found:
ArrayBuffer(2, 2, 3)
在這個例子中,由於從第二行到最後一行的數組散列碼xs已經發生了改變,最後一行的選擇操作將很有可能失敗。因此,基於散列碼的查找函數將會查找另一個位置,而不是xs所存儲的位置。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/equality.html
視圖(Views)
各種容器類自帶一些用於開發新容器的方法。例如map、filter和++。我們將這類方法稱爲轉換器(transformers),餵給它們一個或多個容器,它們就會輸入一個新容器。
有兩個主要途徑實現轉換器(transformers)。一個途徑叫緊湊法,就是一個容器及其所有單元構造成這個轉換器(transformers)。另一個途徑叫鬆弛法或惰性法(lazy),就是一個容器及其所有單元僅僅是構造了結果容器的代理,並且結果容器的每個單元都是按單一需求構造的。
作爲一個鬆弛法轉換器的例子,分析下面的 lazy map操作:
def lazyMap[T, U](coll: Iterable[T], f: T => U) = new Iterable[T] {
def iterator = coll.iterator map f
}
注意lazyMap構造了一個沒有遍歷容器coll(collection coll)所有單元的新容器Iterable。當需要時,函數f 可作用於一個該新容器的迭代器單元。
除了Stream的轉換器是惰性實現的外,Scala的其他容器默認都是用緊湊法實現它們的轉換器。
然而,通常基於容器視圖,可將容器轉換成惰性容器,反之亦可。視圖是代表一些基容器但又可以惰性得構成轉換器(transformers)的一種特殊容器。
從容器轉換到其視圖,可以使用容器相應的視圖方法。如果xs是個容器,那麼xs.view就是同一個容器,不過所有的轉換器都是惰性的。若要從視圖轉換回緊湊型容器,可以使用強制性方法。
讓我們看一個例子。假設你有一個帶有int型數據的vector對象,你想用map函數對它進行兩次連續的操作
scala> val v = Vector(1 to 10: _*)
v: scala.collection.immutable.Vector[Int] =
Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> v map (_ + 1) map (_ * 2)
res5: scala.collection.immutable.Vector[Int] =
Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22)
在最後一條語句中,表達式v map (_ + 1)
構建了一個新的vector對象,該對象被map第二次調用(_ * 2)
而轉換成第3個vector對象。很多情況下,從map的第一次調用構造一箇中間結果有點浪費資源。上述示例中,將map的兩次操作結合成一次單一的map操作執行得會更快些。如果這兩次操作同時可行,則可親自將它們結合成一次操作。但通常,數據結構的連續轉換出現在不同的程序模塊裏。融合那些轉換將會破壞其模塊性。更普遍的做法是通過把vector對象首先轉換成其視圖,然後把所有的轉換作用於該視圖,最後強制將視圖轉換成vector對象,從而避開出現中間結果這種情況。
scala> (v.view map (_ + 1) map (_ * 2)).force
res12: Seq[Int] = Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22)
讓我們按這個步驟一步一步再做一次:
scala> val vv = v.view
vv: scala.collection.SeqView[Int,Vector[Int]] =
SeqView(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
v.view 給出了SeqView對象,它是一個延遲計算的Seq。SeqView有兩個參數,第一個是整型(Int)表示視圖單元的類型。第二個Vector[Int]數組表示當需要強制將視圖轉回時構造函數的類型。
將第一個map 轉換成視圖可得到:
scala> vv map (_ + 1)
res13: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...)
map的結果是輸出SeqViewM(...)
的值。實質是記錄函數map (_ + 1)
應用在vector v數組上的封裝。除非視圖被強制轉換,否則map不會被執行。然而,SeqView
後面的 ‘’M‘’
表示這個視圖包含一個map操作。其他字母表示其他延遲操作。比如‘’S‘’
表示一個延遲的slice操作,而‘’R‘’
表示reverse操作。現在讓我們將第二個map操作作用於最後的結果。
scala> res13 map (_ * 2)
res14: scala.collection.SeqView[Int,Seq[_]] = SeqViewMM(...)
現在得到了包含2個map操作的SeqView
對象,這將輸出兩個‘’M‘’: SeqViewMM(...)
。最後強制轉換最後結果:
scala> res14.force res15: Seq[Int] = Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22)
兩個存儲函數應用於強制操作的執行部分並構造一個新的矢量數組。這樣,沒有中間數據結構是必須的。
需要注意的是靜態類型的最終結果是Seq對象而不是Vector對象。跟蹤類型後我們看到一旦第一個延遲map被應用,就會得到一個靜態類型的SeqViewM[Int, Seq[_]
。就是說,應用於特定序列類型的矢量數組的"knowledge"會被丟失。一些類的視圖的實現需要大量代碼,於是Scala
容器鏈接庫僅主要爲一般的容器類型而不是特殊功能(一個例外是數組:將數組操作延遲會再次給予靜態類型數組的結果)的實現提供視圖。
有2個理由使您考慮使用視圖。首先是性能。你已經看到,通過轉換容器爲視圖可以避免中間結果。這些節省是非常重要的。就像另一個例子,考慮到在一個單詞列表找到第一個迴文問題。迴文就是順讀或倒讀都一樣的單詞。以下是必要的定義:
def isPalindrome(x: String) = x == x.reverse
def findPalidrome(s: Seq[String]) = s find isPalindrome
現在,假設你有一個很長序列的單詞表,你想在這個序列的第一百萬個字內找到迴文。你能複用findPalidrome麼?當然,你可以寫:
findPalindrome(words take 1000000)
這很好地解決了兩個方面問題:提取序列的第一個百萬單詞,找到一個迴文結構。但缺點是,它總是構建由一百萬個字組成的中間序列,即使該序列的第一個單詞已經是一個迴文。所以可能,999 '999個單詞在根本沒被檢查就複製到中間的結果(數據結構中)。很多程序員會在這裏放棄轉而編寫給定參數前綴的尋找回文的自定義序列。但對於視圖(views),這沒必要。簡單地寫:
findPalindrome(words.view take 1000000)
這同樣是一個很好的分選,但不是一個序列的一百萬個元素,它只會構造一個輕量級的視圖對象。這樣,你無需在性能和模塊化之間衡量取捨。
第二個案例適用於遍歷可變序列的視圖。許多轉換器函數在那些視圖提供視窗給部分元素可以非常規更新的原始序列。通過一個示例看看這種情形。讓我們假定有一個數組arr:
scala> val arr = (0 to 9).toArray
arr: Array[Int] = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
你可以在arr數組視圖的一部分裏創建一個子窗體。
scala> val subarr = arr.view.slice(3, 6)
subarr: scala.collection.mutable.IndexedSeqView[
Int,Array[Int]] = IndexedSeqViewS(...)
這裏給出了一個視圖subarr指向從數組arr的第三個元素開始的5個元素組成的子數組。這個視圖沒有拷貝這些元素,而只是提供了它們的一個映射。現在,假設你有一個修改序列元素的方法。例如,下面的negate方法將對給定整數序列的所有元素取反操作:
scala> def negate(xs: collection.mutable.Seq[Int]) =
for (i <- 0 until xs.length) xs(i) = -xs(i)
negate: (xs: scala.collection.mutable.Seq[Int])Unit
假定現在你要對數組arr裏從第3個元素開始的5個元素取反操作。你能夠使用negate方法來做麼?使用視圖,就這麼簡單:
scala> negate(subarr)
scala> arr
res4: Array[Int] = Array(0, 1, 2, -3, -4, -5, 6, 7, 8, 9)
看看發生什麼了,negate方法改變了從數組arr截取元素生成的數組subarr裏面的所有元素。你再次看到視圖(views)在保持模塊化方面的功效。上面的代碼完美地分離了使用方法時如何安排下標順序和使用什麼方法的問題。
看了這些漂亮的視圖應用示例你可能會困惑於爲什麼怎麼還是會有 strict型容器存在了?一個原因是 lazy型容器性能不總是優於strict型容器的。對於較小的容器在視圖裏創建和關閉應用附加開銷通常是大於從避免中間數據結構的增益。一個更重要的原因是,如果延遲操作有副作用,可能導致視圖非常混亂。
這裏有一個使用2.8版本以前的Scala的幾個用戶的例子。在這些版本中, Range類型是延遲型的。所以它表現的效果就像一個視圖。人們試圖創造一些對象像這樣:
val actors = for (i <- 1 to 10) yield actor { ... }
令他們吃驚的是,沒有對象被執行。甚至在後面括號裏的代碼裏無法創建和啓動對象方法。對於爲什麼什麼都沒發生,記住,對上述表達式等價於map應用:
val actors = (1 to 10) map (i => actor { ... })
由於先前的範圍由(1~10)表現得像一個視圖,map的結果又是一個視圖。那就是,沒有元素計算,並且,因此,沒有對象的構建!對象會在整個表達的範圍內被強制創建,但這並不就是對象要完成的工作。
爲了避免這樣的疑惑,Scala 2.8版容器鏈接庫有了更嚴格的規定。除streams 和 views 外所有容器都是strict型的。只有一種途徑將strict型容器轉換成lazy型,那就是採用視圖(view)方法。而唯一可逆的途徑(from lazy to strict)就是採用強制。因此在Scala 2.8版裏actors 對象上面的定義會像預期的那樣,這將創建和啓動10個actors對象。回到先前疑惑處,你可以增加一個明確的視圖方法調用:
val actors = for (i <- (1 to 10).view) yield actor { ... }
總之,視圖是協調性能和模塊化的一個強大工具。但爲了不被延遲利弊評估方面的糾纏,應該在2個方面對視圖進行約束。要麼你將在容器轉換器不產生副作用的純粹的功能代碼裏使用視圖。要麼你將它們應用在所有的修改都是明確的可變容器。最好的規避就是混合視圖和操作,創建新的根接口,同時消除片面影響。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/views.html
Iterators
迭代器不是一個容器,更確切的說是逐一訪問容器內元素的方法。迭代器it的兩個基本操作是next和hasNext。調用it.next()會返回迭代器的下一個元素,並且更新迭代器的狀態。在同一個迭代器上再次調用next,會產生一個新元素來覆蓋之前返回的元素。如果沒有元素可返回,調用next方法會拋出一個NoSuchElementException異常。你可以調用[迭代器]的hasNext方法來查詢容器中是否有下一個元素可供返回。
讓迭代器it逐個返回所有元素最簡單的方法是使用while循環:
while (it.hasNext)
println(it.next())
Scala爲Traversable, Iterable和Seq類中的迭代器提供了許多類似的方法。比如:這些類提供了foreach方法以便在迭代器返回的每個元素上執行指定的程序。使用foreach方法可以將上面的循環縮寫爲:
it foreach println
與往常一樣,for表達式可以作爲foreach、map、withFilter和flatMap表達式的替代語法,所以另一種打印出迭代器返回的所有元素的方式會是這樣:
for (elem <- it) println(elem)
在迭代器或traversable容器中調用foreach方法的最大區別是:當在迭代器中完成調用foreach方法後會將迭代器保留在最後一個元素的位置。所以在這個迭代器上再次調用next方法時會拋出NoSuchElementException異常。與此不同的是,當在容器中調用foreach方法後,容器中的元素數量不會變化(除非被傳遞進來的函數刪除了元素,但不贊成這樣做,因爲這會導致意想不到的結果)。
迭代器的其他操作跟Traversable一樣具有相同的特性。例如:迭代器提供了map方法,該方法會返回一個新的迭代器:
scala> val it = Iterator("a", "number", "of", "words")
it: Iterator[java.lang.String] = non-empty iterator
scala> it.map(_.length)
res1: Iterator[Int] = non-empty iterator
scala> res1 foreach println
1
6
2
5
scala> it.next()
java.util.NoSuchElementException: next on empty iterator
如你所見,在調用了it.map方法後,迭代器it移動到了最後一個元素的位置。
另一個例子是關於dropWhile方法,它用來在迭代器中找到第一個具有某些屬性的元素。比如:在上文所說的迭代器中找到第一個具有兩個以上字符的單詞,你可以這樣寫:
scala> val it = Iterator("a", "number", "of", "words")
it: Iterator[java.lang.String] = non-empty iterator
scala> it dropWhile (_.length < 2)
res4: Iterator[java.lang.String] = non-empty iterator
scala> it.next()
res5: java.lang.String = number
再次注意it在調用dropWhile方法後發生的變化:現在it指向了list中的第二個單詞"number"。實際上,it和dropWhile返回的結果res4將會返回相同的元素序列。
只有一個標準操作允許重用同一個迭代器:
val (it1, it2) = it.duplicate
這個操作返回兩個迭代器,每個都相當於迭代器it的完全拷貝。這兩個iterator相互獨立;一個發生變化不會影響到另外一個。相比之下,原來的迭代器it則被指定到元素的末端而無法再次使用。
總的來說,如果調用完迭代器的方法後就不再訪問它,那麼迭代器的行爲方式與容器是比較相像的。Scala容器庫中的抽象類TraversableOnce使這一特質更加明顯,它是 Traversable 和 Iterator 的公共父類。顧名思義,TraversableOnce 對象可以用foreach來遍歷,但是沒有指定該對象遍歷之後的狀態。如果TraversableOnce對象是一個迭代器,它遍歷之後會位於最後一個元素,但如果是Traversable則不會發生變化。TraversableOnce的一個通常用法是作爲一個方法的參數類型,傳遞的參數既可以是迭代器,也可以是traversable。Traversable類中的追加方法++就是一個例子。它有一個TraversableOnce 類型的參數,所以你要追加的元素既可以來自於迭代器也可以來自於traversable容器。
下面彙總了迭代器的所有操作。
Iterator類的操作
WHAT IT IS | WHAT IT DOES |
---|---|
抽象方法: | |
it.next() | 返回迭代器中的下一個元素,並將位置移動至該元素之後。 |
it.hasNext | 如果還有可返回的元素,返回true。 |
變量: | |
it.buffered | 被緩存的迭代器返回it的所有元素。 |
it grouped size | 迭代器會生成由it返回元素組成的定長序列塊。 |
xs sliding size | 迭代器會生成由it返回元素組成的定長滑動窗口序列。 |
複製: | |
it.duplicate | 會生成兩個能分別返回it所有元素的迭代器。 |
加法: | |
it ++ jt | 迭代器會返回迭代器it的所有元素,並且後面會附加迭代器jt的所有元素。 |
it padTo (len, x) | 首先返回it的所有元素,追加拷貝x直到長度達到len。 |
Maps: | |
it map f | 將it中的每個元素傳入函數f後的結果生成新的迭代器。 |
it flatMap f | 針對it指向的序列中的每個元素應用函數f,並返回指向結果序列的迭代器。 |
it collect f | 針對it所指向的序列中的每一個在偏函數f上有定義的元素應用f,並返回指向結果序列的迭代器。 |
轉換(Conversions): | |
it.toArray | 將it指向的所有元素歸入數組並返回。 |
it.toList | 把it指向的所有元素歸入列表並返回 |
it.toIterable | 把it指向的所有元素歸入一個Iterable容器並返回。 |
it.toSeq | 將it指向的所有元素歸入一個Seq容器並返回。 |
it.toIndexedSeq | 將it指向的所有元素歸入一個IndexedSeq容器並返回。 |
it.toStream | 將it指向的所有元素歸入一個Stream容器並返回。 |
it.toSet | 將it指向的所有元素歸入一個Set並返回。 |
it.toMap | 將it指向的所有鍵值對歸入一個Map並返回。 |
拷貝: | |
it copyToBuffer buf | 將it指向的所有元素拷貝至緩衝區buf。 |
it copyToArray(arr, s, n) | 將it指向的從第s個元素開始的n個元素拷貝到數組arr,其中後兩個參數是可選的。 |
尺寸信息: | |
it.isEmpty | 檢查it是否爲空(與hasNext相反)。 |
it.nonEmpty | 檢查容器中是否包含元素(相當於 hasNext)。 |
it.size | it可返回的元素數量。注意:這個操作會將it置於終點! |
it.length | 與it.size相同。 |
it.hasDefiniteSize | 如果it指向的元素個數有限則返回true(缺省等同於isEmpty) |
按下標檢索元素: | |
it find p | 返回第一個滿足p的元素或None。注意:如果找到滿足條件的元素,迭代器會被置於該元素之後;如果沒有找到,會被置於終點。 |
it indexOf x | 返回it指向的元素中index等於x的第一個元素。注意:迭代器會越過這個元素。 |
it indexWhere p | 返回it指向的元素中下標滿足條件p的元素。注意:迭代器會越過這個元素。 |
子迭代器: | |
it take n | 返回一個包含it指向的前n個元素的新迭代器。注意:it的位置會步進至第n個元素之後,如果it指向的元素數不足n個,迭代器將指向終點。 |
it drop n | 返回一個指向it所指位置之後第n+1個元素的新迭代器。注意:it將步進至相同位置。 |
it slice (m,n) | 返回一個新的迭代器,指向it所指向的序列中從開始於第m個元素、結束於第n個元素的片段。 |
it takeWhile p | 返回一個迭代器,指代從it開始到第一個不滿足條件p的元素爲止的片段。 |
it dropWhile p | 返回一個新的迭代器,指向it所指元素中第一個不滿足條件p的元素開始直至終點的所有元素。 |
it filter p | 返回一個新迭代器 ,指向it所指元素中所有滿足條件p的元素。 |
it withFilter p | 同it filter p 一樣,用於for表達式。 |
it filterNot p | 返回一個迭代器,指向it所指元素中不滿足條件p的元素。 |
拆分(Subdivision): | |
it partition p | 將it分爲兩個迭代器;一個指向it所指元素中滿足條件謂詞p的元素,另一個指向不滿足條件謂詞p的元素。 |
條件元素(Element Conditions): | |
it forall p | 返回一個布爾值,指明it所指元素是否都滿足p。 |
it exists p | 返回一個布爾值,指明it所指元素中是否存在滿足p的元素。 |
it count p | 返回it所指元素中滿足條件謂詞p的元素總數。 |
摺疊(Fold): | |
(z /: it)(op) | 自左向右在it所指元素的相鄰元素間應用二元操作op,初始值爲z。 |
(it :\ z)(op) | 自右向左在it所指元素的相鄰元素間應用二元操作op,初始值爲z。 |
it.foldLeft(z)(op) | 與(z /: it)(op)相同。 |
it.foldRight(z)(op) | 與(it :\ z)(op)相同。 |
it reduceLeft op | 自左向右對非空迭代器it所指元素的相鄰元素間應用二元操作op。 |
it reduceRight op | 自右向左對非空迭代器it所指元素的相鄰元素間應用二元操作op。 |
特殊摺疊(Specific Fold): | |
it.sum | 返回迭代器it所指數值型元素的和。 |
it.product | 返回迭代器it所指數值型元素的積。 |
it.min | 返回迭代器it所指元素中最小的元素。 |
it.max | 返回迭代器it所指元素中最大的元素。 |
拉鍊方法(Zippers): | |
it zip jt | 返回一個新迭代器,指向分別由it和jt所指元素一一對應而成的二元組序列。 |
it zipAll (jt, x, y) | 返回一個新迭代器,指向分別由it和jt所指元素一一對應而成的二元組序列,長度較短的迭代器會被追加元素x或y,以匹配較長的迭代器。 |
it.zipWithIndex | 返回一個迭代器,指向由it中的元素及其下標共同構成的二元組序列。 |
更新: | |
it patch (i, jt, r) | 由it返回一個新迭代器,其中自第i個元素開始的r個元素被迭代器jt所指元素替換。 |
比對: | |
it sameElements jt | 判斷迭代器it和jt是否依次返回相同元素注意:it和jt中至少有一個會步進到終點。 |
字符串(String): | |
it addString (b, start, sep, end) | 添加一個字符串到StringBuilder b,該字符串以start爲前綴、以end爲後綴,中間是以sep分隔的it所指向的所有元素。start、end和sep都是可選項。 |
it mkString (start, sep, end) | 將it所指所有元素轉換成以start爲前綴、end爲後綴、按sep分隔的字符串。start、sep、end都是可選項。 |
帶緩衝的迭代器
有時候你可能需要一個支持“預覽”功能的迭代器,這樣我們既可以看到下一個待返回的元素,又不會令迭代器跨過這個元素。比如有這樣一個任務,把迭代器所指元素中的非空元素轉化成字符串。你可能會這樣寫:
def skipEmptyWordsNOT(it: Iterator[String]) =
while (it.next().isEmpty) {}
但仔細看看這段代碼,就會發現明顯的錯誤:代碼確實會跳過空字符串,但同時它也跳過了第一個非空字符串!
要解決這個問題,可以使用帶緩衝能力的迭代器。[BufferedIterator]類是[Iterator]的子類,提供了一個附加的方法,head。在BufferedIterator中調用head 會返回它指向的第一個元素,但是不會令迭代器步進。使用BufferedIterator,跳過空字符串的方法可以寫成下面這樣:
def skipEmptyWords(it: BufferedIterator[String]) =
while (it.head.isEmpty) { it.next() }
通過調用buffered方法,所有迭代器都可以轉換成BufferedIterator。參見下例:
scala> val it = Iterator(1, 2, 3, 4)
it: Iterator[Int] = non-empty iterator
scala> val bit = it.buffered
bit: java.lang.Object with scala.collection.
BufferedIterator[Int] = non-empty iterator
scala> bit.head
res10: Int = 1
scala> bit.next()
res11: Int = 1
scala> bit.next()
res11: Int = 2
注意,調用BufferedIterator bit
的head方法不會令它步進。因此接下來的bit.next()
返回的元素跟bit.head
相同。
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/iterators.html
從頭定義新容器(Creating Collections From Scratch)
我們已經知道List(1, 2, 3)
可以創建出含有三個元素的列表,用Map('A' -> 1, 'C' -> 2)
可以創建含有兩對綁定的映射。實際上各種Scala容器都支持這一功能。任意容器的名字後面都可以加上一對帶參數列表的括號,進而生成一個以這些參數爲元素的新容器。不妨再看一些例子:
Traversable() // 一個空的Traversable對象
List() // 空列表
List(1.0, 2.0) // 一個以1.0、2.0爲元素的列表
Vector(1.0, 2.0) // 一個以1.0、2.0爲元素的Vector
Iterator(1, 2, 3) // 一個迭代器,可返回三個整數
Set(dog, cat, bird) // 一個包含三個動物的集合
HashSet(dog, cat, bird) // 一個包含三個同樣動物的HashSet
Map('a' -> 7, 'b' -> 0) // 一個將字符映射到整數的Map
實際上,上述每個例子都被“暗地裏”轉換成了對某個對象的apply方法的調用。例如,上述第三行會展開成如下形式:
List.apply(1.0, 2.0)
可見,這裏調用的是List類的伴生對象的apply方法。該方法可以接受任意多個參數,並將這些參數作爲元素,生成一個新的列表。在Scala標準庫中,無論是List、Stream、Vector等具體的實現類還是Seq、Set、Traversable等抽象基類,每個容器類都伴一個帶apply方法的伴生對象。針對後者,調用apply方法將得到對應抽象基類的某個默認實現,例如:
scala > List(1,2,3)
res17: List[Int] = List(1, 2, 3)
scala> Traversable(1, 2, 3)
res18: Traversable[Int] = List(1, 2, 3)
scala> mutable.Traversable(1, 2, 3)
res19: scala.collection.mutable.Traversable[Int] = ArrayBuffer(1, 2, 3)
除了apply方法,每個容器類的伴生對象還定義了一個名爲empty的成員方法,該方法返回一個空容器。也就是說,List.empty
可以代替List()
,Map.empty
可以代替Map()
,等等。
Seq的子類同樣在它們伴生對象中提供了工廠方法,總結如下表。簡而言之,有這麼一些:
concat,將任意多個Traversable容器串聯起來
fill 和 tabulate,用於生成一維或者多維序列,並用給定的初值或打表函數來初始化。
range,用於生成步長爲step的整型序列,並且iterate,將某個函數反覆應用於某個初始元素,從而產生一個序列。
序列的工廠方法
WHAT IT IS | WHAT IT DOES |
---|---|
S.emtpy | 空序列 |
S(x, y, z) | 一個包含x、y、z的序列 |
S.concat(xs, ys, zs) | 將xs、ys、zs串街起來形成一個新序列。 |
S.fill(n) {e} | 以表達式e的結果爲初值生成一個長度爲n的序列。 |
S.fill(m, n){e} | 以表達式e的結果爲初值生成一個維度爲m x n的序列(還有更高維度的版本) |
S.tabulate(n) {f} | 生成一個廠素爲n、第i個元素爲f(i)的序列。 |
S.tabulate(m, n){f} | 生成一個維度爲m x n,第(i, j)個元素爲f(i, j)的序列(還有更高維度的版本)。 |
S.range(start, end) | start, start + 1, ... end-1的序列。(譯註:注意始左閉右開區間) |
S.range(start, end, step) | 生成以start爲起始元素、step爲步長、最大值不超過end的遞增序列(左閉右開)。 |
S.iterate(x, n)(f) | 生成一個長度爲n的序列,其元素值分別爲x、f(x)、f(f(x))、…… |
更詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/creating-collections-from-scratch.html
Java和Scala容器的轉換(Conversions Between Java and Scala Colle)
和Scala一樣,Java同樣提供了豐富的容器庫,Scala和Java容器庫有很多相似點,例如,他們都包含迭代器、可迭代結構、集合、 映射和序列。但是他們有一個重要的區別。Scala的容器庫特別強調不可變性,因此提供了大量的新方法將一個容器變換成一個新的容器。
某些時候,你需要將一種容器類型轉換成另外一種類型。例如,你可能想要像訪問Scala容器一樣訪問某個Java容器,或者你可能想將一個Scala容器像Java容器一樣傳遞給某個Java方法。在Scala中,這是很容易的,因爲Scala提供了大量的方法來隱式轉換所有主要的Java和Scala容器類型。其中提供瞭如下的雙向類型轉換:
Iterator <=> java.util.Iterator
Iterator <=> java.util.Enumeration
Iterable <=> java.lang.Iterable
Iterable <=> java.util.Collection
mutable.Buffer <=> java.util.List
mutable.Set <=> java.util.Set
mutable.Map <=> java.util.Map
mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap
使用這些轉換很簡單,只需從JavaConversions對象中import它們即可。
scala> import collection.JavaConversions._
import collection.Java.Conversions._
import之後,就可以在Scala容器和與之對應的Java容器之間進行隱式轉換了
scala> import collection.mutable._
import collection.mutable._
scala> val jul: java.util.List[Int] = ArrayBuffer(1, 2, 3)
jul: java.util.List[Int] = [1, 2, 3]
scala> val buf: Seq[Int] = jul
buf: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3)
scala> val m: java.util.Map[String, Int] = HashMap("abc" -> 1, "hello" -> 2)
m: java.util.Map[String, Int] = {hello=2, abc=1}
在Scala內部,這些轉換是通過一系列“包裝”對象完成的,這些對象會將相應的方法調用轉發至底層的容器對象。所以容器不會在Java和Scala之間拷貝來拷貝去。一個值得注意的特性是,如果你將一個Java容器轉換成其對應的Scala容器,然後再將其轉換回同樣的Java容器,最終得到的是一個和一開始完全相同的容器對象(譯註:這裏的相同意味着這兩個對象實際上是指向同一片內存區域的引用,容器轉換過程中沒有任何的拷貝發生)。
還有一些Scala容器類型可以轉換成對應的Java類型,但是並沒有將相應的Java類型轉換成Scala類型的能力,它們是:
Seq => java.util.List
mutable.Seq => java.util.List
Set => java.util.Set
Map => java.util.Map
因爲Java並未區分可變容器不可變容器類型,所以,雖然能將scala.immutable.List
轉換成java.util.List
,但所有的修改操作都會拋出“UnsupportedOperationException”。參見下例:
scala> jul = List(1, 2, 3)
jul: java.util.List[Int] = [1, 2, 3]
scala> jul.add(7)
java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:131)
更多詳細內容參考官網:http://docs.scala-lang.org/overviews/collections/conversions-between-java-and-scala-collections.html