Scala學習(十三)集合

1.主要集合特質

構成Scala集合繼承層級最重要的特質git

Iterable指的是那些可以生成用來訪問集合中全部元素的Iterator的集合算法

Seq是一個有前後次序的值得序列,好比數組或列表。IndexedSeq容許咱們經過整型的下標快速地訪問任意元素。舉例來講ArrayBuffer是帶下標的,但鏈表不是。數組

Set是一組沒有前後次序的值。在SortedSet中,元素以某種排過序的順序被訪問。緩存

Map是一組對偶。SortedMap按照鍵的排序訪問其中的實體。安全

這個繼承層級和Java很類似,同時有些不錯的改進:數據結構

  1. 映射隸屬於同一繼承層級而不是一個單獨的層級關係。
  2. IndexedSeq是數組的超類型,但不是列表的超類型,以便於區分。

每一個Scala集合特質或類都有一個帶有apply方法的伴生對象,這個apply方法能夠用來構建該集合的實例,而不用使用new,這樣的設計叫作"統一建立原則"。併發

2.可變和不可變集合

Scala同時支持可變和不可變的集合。Scala優先採用不可變集合,所以你能夠安全的共享其引用。任何對不可變集合的修改操做都返回的是一個新的不可變集合,它們共享大部分元素。app

3.序列

最重要的不可變序列函數

Vector是ArrayBuffer的不可變版本,和C++的Vector同樣,能夠經過下標快速的隨機訪問。而Scala的Vector是以樹形結構的形式實現的,每一個節點能夠有不超過32個子節點。這樣對於有100萬的元素的向量而言,只須要4層節點。
Range表示一個整數序列,例如0,1,2,3,4,5 或 10,20,30 . Rang對象並不存儲全部值而只是起始值、結束值和增值。spa

最有用的可變序列

咱們以前介紹了數組緩衝。而棧、隊列、優先級隊列等都是標準的數據結構,用來實現特淡定的算法。可是Scala鏈表類有些特殊,它們和Java中或數據結構課程中的接觸到的連表不大同樣。

4.列表

 

在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)    //h 是lst.head 而t是lst.tail
}

注意第二種遍歷模式中的::操做符,它將列表"析構"成了頭部和尾部。

說明:遞歸之因此這麼天然,是由於列表的尾部正好又是一個列表。

若是你想要當場修改可變列表元素,你能夠用ListBuffer,這是一個由鏈表支撐的數據結構,包含一個紙箱最後一個節點的引用。這讓它能夠高效地從任意一端添加或移除元素。

不過在ListBuffer中添加或移除元素並不高效,這時你能夠考慮Java的LinkedList。

說明:Scala中LinkedListheDoubleLinkedList類(他們都已通過時了)還有一個內部的MutableList類,這些是你不該該用的。

5.集

集是不重複的元素的集合。嘗試將已有元素加入沒有效果。例如:

Set(2, 0, 1) + 1 和 Set(2, 0, 1) 是同樣的

和列表不一樣,集並不保留元素插入的順序,默認狀況下,集以哈希集實現。

其元素根據hashCode方法的值進行組織。(Scala和Java同樣。每一個對象都有hashCode方法)

在哈希集中查找元素要比在數組或列表中快得多。

而鏈式哈希集能夠記住元素插入的順序,它會維護一個鏈表來達到這個目的。

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

對於SortedSet已排序的集使用紅黑樹實現的。

位集(bit set)是集的一種實現,以一個字節序列的方式存放非負整數。若是集中有i,則第i個字位是1.這是很搞笑的實現,只要最大元素不是特別的大。Scala提供了可變的和不可變的兩個BitSet類。

集的一些常見操做:contains方法檢查某個集是否包含給定的值。subsetOf 方法檢查某個集當中的全部元素是否都被另外一個集包含。

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 Set(1,2,3,5,7,9)

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

digits diff primes // Set(1, 9)
// 等同於
digits -- primes // 或digits &~ primes Set(1, 9)

6.用於添加或去除元素的操做符

下面展現了爲各類集合類型定義的用於添加或去除元素的操做符

通常而言, + 用於將元素添加到無前後次序的集合,而+: 和 :+ 則是將元素添加到有前後次序的集合的開頭或是結尾。

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

如你所見,Scala提供了許多用於添加和移除元素的操做符。如下是一個彙總:

  1. 向後(:+)或向前(+:)追加元素到序列當中。
  2. 添加(+)元素到無前後次序的集合中。
  3. 用 - 移除元素。
  4. 用++和–來批量添加和移除元素。
  5. 對於列表,有限使用 :: 和 ::: 。
  6. 改值操做有 +=、++=和 --=。
  7. 對於集合,我更喜歡++、&和–。
  8. 我儘可能不用++:、+=:和++=:。

說明:對於列表,你能夠用+:而不是::來保持與其餘集合操做的一致性,但有一個例外,匹配模式不認 +: 操做符。

7.經常使用方法

下表給出了Iterator最重要方法的概覽,按功能點排序。

Seq特質又在Iterator的基礎上新添加了方法。

注:這些方法從不改變原有集合。它們返回與元集合相同類型的新集合。這有時被叫作「統一返回類型原則「。

8.將函數映射到集合

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")

transform方法是map的等效操做,只不過是當場執行(而不是交出新的集合)。它應用於可變集合並將每一個元素都替換成函數的結果。以下代碼將全部的緩衝區元素改爲大寫:

val buffer = ArrayBuffer("Petter","Paul","Mary")

buffer.transform(_.toUpperCase)

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()

groupBy方法交出的是這樣一個映射:他的鍵是函數(求值後)的值,而是那些函數求值獲得給丁健的元素集合。例如:

val words = ...

val map = words.groupBy(_.substring(0,1).toUpperCase)

foreach方法將函數應用到各個元素但不關心函數的返回值。

names.foreach(println)

9.化簡、摺疊和掃描

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 /: List(1,7,2,9)) 或者 0 - 1 -7 - 2 - 9
List(1,7,2,9).foldRight(0)(_ - _) // 可用 :\ 代替,至關於(List(1,7,2,9) :\ 0) 或者 1 - (7 - (2 - (9 - 0)))

說明:初始值和操做符是兩個分開的柯里化參數,這樣Scala就能用初始值的類型來推斷出操做符的類型定義。舉例來講:在List(1,7,2,9).foldLeft("")(_ + _)中,初始值是一個字符串,所以操做符一定是一個類型定義爲(String,Int) => String的函數。

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

說明:任何while循環均可以用摺疊來代替。構建一個把循環中被更新的全部變量結合在一塊兒的數據結構,而後定義一個操做,實現循環中的一步。我並非說這樣作老是好的,但你可能以爲循環和改值能夠像這樣被消除是一件頗有趣的事。

10.拉鍊操做

前面的章節已經講過拉鍊操做。除了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) )

11.迭代器

你能夠用iterator方法從集合得到一個迭代器。這種作法並不像在Java中那麼廣泛,你一般能夠更容易的獲得你所須要的結果。

迭代器的好處就是你不用將開銷很大的集合所有讀進內存。例如讀取文件操做,Source.fromFile產出一個迭代器,使用hasNext和next方法來遍歷:

while (iter.hasNext)
對 iter.next() 執行某種操做

這裏要注意迭代器多指向的位置。在調用了map、filter、count、sum、length等方法後,迭代器將位於集合的尾端,你不能再繼續使用它。而對於其餘方法而言,好比find或take,迭代器位於已經找到元素或已取得元素以後。

12.流

流是一個尾部被懶計算的不可變列表-----也就是說,只有當你須要時它纔會被計算。

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 。

你能夠從且待期構造一個流。舉例來講,Source.getLines方法返回一個Iterator[String]。用這個迭代器,對於每一行你只能訪問一次。而流將緩存訪問過的每一行,容許你從新訪問他們。

13.懶視圖

view方法返回一個老是被懶執行的集合,例如:

val p = (1 to 1000).view.map(x => x * x).filter(x => x.toString == x.toString.reverse)

將交出一個未被求值的集合(不像流,這裏連第一個元素都未被求值)。當你執行以下代碼時:

p.take(10).mkString(",")

將生成足夠多的二次方,直到咱們獲得10個迴文。跟流不一樣,視圖不會緩存任何值。再次調用相同的代碼,整個計算會從新開始。

和流同樣force方法能夠對懶視圖強制求值。你將獲得與元集合相同類型的新集合。

注:apply方法會強制對整個視圖求值。因此不要直接調用lazyView(i),而是用該調用lazyView.take(i).last

14.與Java集合相互操做

15.並行集合

集合的par方法產出當前集合的一個並行實現,例如sum求和,多個線程能夠併發的計算不一樣區塊的和,在最後這部分結果被彙總到一塊兒。

coll.par.sum

得出coll中全部偶數的數量:

coll.par.count(_ % 2 ==0)

你能夠經過對要遍歷的集合應用.par並行化for循環,就像這樣:

for(i <- (0 until 1000).par) print(s" $i")

能夠看到數字將不是按順序輸出而是按照線程產出順序輸出,若是咱們不.par並行化的話將會是順序輸出

而在for/yield循環中,結果是依次組裝的

注:若是並行運算修改了共享變量,則結果沒法預知。舉例來講,不要更新一個共享計數器:

var count = 0

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

par方法返回的並行集合屬於擴展自ParSeq、ParSet或ParMap特質的類型。這些並非Seq、Set或Map的子類型,你不能像一個預期順序集合的方法傳入並行集合。你能夠用 seq 方法將並行集合轉換爲順序集合。

val result = coll.par.filter(p).seq

並不是全部的方法都能被並行化。例如,reduceLeft和reduceRight要求操做符都要按照順序應用。

說明:默認狀況下,並行集合使用全局的fork-join線程池,該線程池很是適合於高處理器開銷的計算。若是你執行的並行步驟包含阻塞調用,就應該另選一種"執行上下文"。

相關文章
相關標籤/搜索