官术网_书友最值得收藏!

2.2 Scala基礎知識

本節介紹Scala的基礎編程知識,包括基本數據類型和變量的定義、輸入輸出語句、流程控制結構和數據結構等。

2.2.1 基本數據類型和變量

1.基本數據類型

表2-1列出了Scala的9種基本數據類型及其取值范圍。其中,類型Byte、Short、Int、Long和Char統稱為整數類型,Float和Double被稱為浮點數類型??梢钥闯?,Scala與 Java有著相同的基本數據類型,只是類型修飾符的首字母大小寫不同,Scala 中所有基本類型的首字母都采用大寫,而Java中除了字符串類型用首字母大寫的String,其他八種基本類型都采用首字母小寫的修飾符。

Scala 是一門純粹的面向對象的語言,每個值都是對象,也就是說 Scala 沒有 Java 中的原生類型,表2-1中列出的數據類型都有相應的類與之對應。在Scala中,除了String類型是在java.lang包中被聲明之外,其余類型都是包 scala 的成員。例如,Int 的全名是 scala.Int。由于包 scala 和java.lang 的所有成員都被每個 Scala 源文件自動引用,因此可以省略包名,而只用 Int、Boolean等簡化名。

表2-1 Scala的基本數據類型

除了以上9種基本類型,Scala還提供了一個Unit類型,類似Java中的void類型,表示“什么都不是”,主要作為不返回任何結果的函數的返回類型。

2.字面量

字面量是直接在源代碼里書寫常量值的一種方式。不同類型的字面量書寫語法如下:

整數字面量

整數字面量有兩種格式:十進制和十六進制。十進制數開始于非零數字,十六進制數開始于0x或0X前綴。需要注意的是,不論用什么進制的字面量進行初始化,Scala的Shell始終打印輸出十進制整數值。整型字面量被編譯器解釋為 Int 類型,如果需要表示 Long,需要在數字后面添加大寫字母L或者小寫字母l作為后綴。

浮點數字面量

浮點數字面量是由十進制數字、小數點和可選的E 或e 及指數部分組成。如果以F或f結尾,會被編譯器解釋為Float類型,否則就是Double類型。

布爾型字面量

布爾型只有true和false兩種字面量。

字符及字符串字面量

字符字面量是在半角單引號之間的任何 Unicode 字符,還可以用反斜杠“\”表示轉義字符。字符串字面量用半角雙引號包括一系列字符表示,如果需要表示多行文本,則用三個雙引號包括。舉例如下:

scala> val c='A'

c: Char = A

scala> var c1='\u0045'

c1: Char = E

scala> c1='\''

c1: Char = '

scala> val s = "hello world"

s: String = hello world

scala> val ss = """ the first line

 | the second line

 | the third line"""

ss: String =

" the first line

the second line

the third line"

Unit字面量

Unit類型只有一個唯一的值,用空的原括號表示,即()。

3.操作符

Scala為它的基本類型提供了豐富的操作符集,包括:

算術運算符:加(+)、減(-)、乘(*)、除(/)、余數(%);

關系運算符:大于(>)、小于(<)、等于(=)、不等于(!=)、大于等于(>=)、小于等于(<=);

邏輯運算符:邏輯與(&&)、邏輯或(||)、邏輯非(!);

位運算符:按位與(&)、按位或(|)、按位異或(^)、按位取反(~)、左移(<<)、右移(>>)、無符號右移(>>>)。

賦值運算符:“=”及其與其他運算符結合的擴展賦值運算符,例如+=、%=。

需要強調的是,盡管這些基本操作符在使用上與Java基本一致,但是,Scala的操作符實際上是方法,也就是說,在Scala中,每個操作都是方法調用,操作符不過是對象方法調用的一種簡寫形式。例如,5 + 3和5.+(3)是等價的,因為Scala作為一門純粹的面向對象語言,它的每個值都是一個對象,即這里的數值5也是一個Int類型的對象,由于Int 類有一個名為“+”的方法,它接收一個Int型參數并返回一個Int型的結果,因此,5.+(3)就表示在5這個對象上調用名稱為“+”的方法,把3作為參數傳遞給該方法,完成加法計算。實際上,Int類還包含了許多帶不同參數類型的重載加法方法。例如,有一個名為“+”的、參數和返回類型都為Double的方法。所以,5+3.5會返回Double型的8.5,相當于調用了5.+(3.5)。另外,與Java不同的是,Scala中各種賦值表達式的值都是Unit類型,因此,盡管“a=b=5”是合法的語句,但不是表示將a和b的值都賦值為5;實際上,執行該語句時,首先執行賦值表達式b=5,使得b的值變為5,b=5這個賦值表達式的值是Unit類型,這樣a就成為Unit類型。

Scala操作符的優先級和Java基本相同,從高到低基本遵循以下順序:

算術運算符 > 關系運算符 > 邏輯運算符 > 賦值運算符

唯一的例外是,邏輯非(!)有比算術運算符更高的優先級。但是,在實際應用中,沒有必要記住所有操作符之間的優先級順序,推薦的做法是,除了不言自明的優先級以外(例如,乘除法優先級比加減法高),盡量使用括號去厘清表達式中操作符的優先級。

對于基本數據類型,除了以上提到的各種操作符外,Scala還提供了許多常用運算的方法,只是這些方法不是在基本類里面定義,而是被封裝到一個對應的富包裝類中。表2-1中每個基本類型都有一個對應的富包裝類。例如,Int有一個對應的RichInt類、String有一個對應的RichString類,這些富包裝類位于包scala.runtime中。當對一個基本數據類型的對象調用其富包裝類提供的方法時,Scala會自動通過隱式轉換,將該對象轉換為對應的富包裝類型,然后再調用相應的方法。例如,執行語句3 max 5時,Scala檢測到基本類型Int沒有提供max方法,但是Int的富包裝類RichInt具有max方法,這時,Scala會自動將3這個對象轉換為RichInt類型,然后調用RichInt的max方法,并將5作為參數傳給該方法,最后返回的結果是Int型的5。

4.變量

盡管 Scala 有多種基本數據類型,但是從聲明變量的角度看,Scala 只有兩種類型的變量,分別使用關鍵字val和var進行聲明。對于用val聲明的變量,在聲明時就必須被初始化,而且初始化以后就不能再賦新的值;對于用var聲明的變量,是可變的,可以被多次賦值。聲明一個變量的基本語法為:

val 變量名:數據類型 = 初始值

var 變量名:數據類型 = 初始值

Scala的這種語法結構與Java中“變量類型 變量名=值”的語法結構有所區別。同時,Scala提供了一種類型推斷機制(Type Inference),它會根據初始值自動推斷變量的類型,這使得定義變量時可以省略具體的數據類型及其前面的冒號。例如,語句var str= "Hello world"與var str : String ="Hello world"的作用是一樣的,因為使用了一個字符串文本初始化變量str,Scala可以自動推斷出str的類型是String。同理,var i=1和var i:Int=1也是等價的。但是,如果需要將i定義為浮點型,則必須顯式指定類型:var i:Double=1,或者用浮點型的值初始化:var i=1.0。

需要注意的是,在 REPL 環境下,可以重復使用同一個變量名來定義變量,而且變量前的修飾符和其類型都可以不一致,REPL會以最新的一個定義為準,例如:

scala> val a = "Xiamen University"

a: String = Xiamen University

scala> var a = 50

a: Int = 50

2.2.2 輸入/輸出

1.控制臺輸入輸出語句

為了從控制臺讀寫數據,可以使用以read為前綴的方法,包括:readInt、readDouble、readByte、readShort、readFloat、readLong、readChar、readBoolean及readLine,分別對應9種基本數據類型,其中,前8種方法沒有參數,readLine可以不提供參數,也可以帶一個字符串參數的提示。所有這些函數都屬于對象scala.io.StdIn的方法,使用前必須導入,或者直接用全稱進行調用。使用示例如下:scala> import io.StdIn._

import io.StdIn._

scala> var i=readInt()

54

i: Int = 54

scala> var f=readFloat

1.618

f: Float = 1.618

scala> var b=readBoolean

true

b: Boolean = true

scala> var str=readLine("please input your name:")

please input your name:Li Lei

str: String = Li Lei

需要注意的是,在Scala的REPL中,從鍵盤讀取數據時,看不到用戶的輸入,需要按回車以后才能看到效果。

為了向控制臺輸出信息,常用的兩個函數是print()和println(),可以直接輸出字符串或者其他數據類型,兩個函數唯一的區別是,后者輸出結束時,會默認加一個換行符,而前者沒有。例如:

scala> val i=345

i: Int = 345

scala> print("i=");print(i) //兩條語句位于同一行,不能省略中間的分號

i=345

scala> println("hello ");println("world")

hello

world

此外,Scala還帶有C語言風格的格式化字符串的printf函數,例如:

scala> val i = 34

i: Int = 34

scala> val f=56.5

f: Double = 56.5

scala> printf("I am %d years old and weight %.1f Kg.",i,f)

I am 34 years old and weight 56.5 kg.

上述提到的三個輸出方法(print、println和printf)都是在對象Predef中定義的,該對象在默認情況下會自動被所有Scala程序引用,因此,可以直接使用Predef對象提供的print、println和printf等方法,而無需使用scala.Predef.println("Hello World")這種形式。另外,Scala提供了字符串插值機制,以方便在字符串字面量中直接嵌入變量的值。為了構造一個插值字符串,只需要在字符串字面量前加一個“s”字符或“f”字符,然后,在字符串中即可以用“$”插入變量的值,s插值字符串不支持格式化,f插值字符串支持在$變量后再跟格式化參數,例如:

scala> val i=10

i: Int = 10

scala> val f=3.5452

f: Double = 3.5452

scala> val s="hello"

s: String = hello

scala> println(s"$s:i=$i,f=$f") //s插值字符串

hello:i=10,f=3.5452

scala> println(f"$s:i=$i%-4d,f=$f%.1f") //f插值字符串

hello:i=10 ,f=3.5

2.讀寫文件

Scala使用類java.io.PrintWriter實現文本文件的創建與寫入。該類由Java庫提供,這正好體現了Scala與Java的互操作性。PrintWriter類提供了print和println兩種寫方法,其用法與向控制臺輸出數據所采用的print和println完全一樣。例如:

scala> import java.io.PrintWriter

scala> val outputFile = new PrintWriter("test.txt")

scala> outputFile.println("Hello World")

scala> outputFile.print("Spark is good")

scala> outputFile.close()

上面語句中,new PrintWriter("test.txt")中使用了相對路徑地址,這意味著,文件test.txt就會被保存到啟動Scala REPL時的當前目錄下。比如,如果在“/usr/local/scala”目錄下使用scala命令啟動進入了Scala REPL,則test.txt會被保存到“/usr/local/scala”目錄下。如果要把文件保存到一個指定的目錄下,就需要在new PrintWriter()的圓括號中給出文件路徑全稱,比如,new PrintWriter("/usr/local/scala/mycode/output.txt")。

盡管PrintWriter類也提供了printf函數,但是,它不能實現數值類型的格式化寫入。為了實現數值類型的格式化寫入,可以使用String類的format方法,或者用f插值字符串,例如:

scala> import java.io.PrintWriter

scala> val outputFile = new PrintWriter("test.txt")

scala> val i = 9

scala> outputFile.print("%3d --> %d\n".format(i,i*i))

scala> outputFile.println(f"$i%3d --> ${i*i}%d") //與上句等效

scala> outputFile.close()

Scala使用類scala.io.Source實現對文件的讀取,最常用的方法是getLines方法,它會返回一個包含所有行的迭代器(迭代器是一種數據結構,將在“2.2.4 數據結構”中介紹)。下面是從一個文件讀出所有行并輸出的實例代碼:

scala> import scala.io.Source

scala> val inputFile = Source.fromFile("test.txt")

scala> for (line <- inputFile.getLines()) println(line)

scala> inputFile.close()

2.2.3 控制結構

同各種高級語言一樣,Scala也包括了內建的選擇控制結構和循環控制結構。其中,選擇結構包括if語句,循環結構包括for語句和while語句。另外,Scala也有內建的異常處理結構try-catch。

1.if條件表達式

if語句用來實現兩個分支的選擇結構,基本語法結構為:

if (表達式) {

  語句塊1

}

else {

  語句塊2

}

執行if語句時,會首先檢查if條件表達式是否為真,如果為真,就執行語句塊1,如果為假,就執行語句塊2。例如:

scala> val x = 6

x: Int = 6

scala> if (x>0) {println("This is a positive number")

 | } else {

 | println("This is not a positive number")

 | }

This is a positive number

Scala與Java類似,if結構中else子句是可選的,而且if子句和else子句中都支持多層嵌套if結構。例如:

scala> val x = 3

x: Int = 3

scala> if (x>0) {

 | println("This is a positive number")

 | } else if (x==0) {

 | println("This is a zero")

 | } else {

 | println("This is a negative number")

 | }

This is a positive number

與Java不同的是,Scala中的if表達式會返回一個值,因此,可以將if表達式賦值給一個變量,這與Java中的三元操作符“?:”有些類似,例如:

scala> val a = if (6>0) 1 else -1

a: Int = 1

2.while循環

Scala的while循環結構和Java的完全一樣,包括以下兩種基本結構,只要表達式為真,循環體就會被重復執行,其中,do-while循環至少被執行一次。

while (表達式){

    循環體

}

或者

do{

    循環體

}while (表達式)

3.for循環表達式

與Java的for循環相比,Scala的for循環在語法表示上有較大的區別,同時,for也不是while循環的一個替代者,而是提供了各種容器遍歷的強大功能,用法也更靈活(容器的概念,將在“2.2.4 數據結構”中介紹)。for 循環最簡單的用法就是對一個容器的所有元素進行枚舉,基本語法結構為:

for (變量 <- 表達式) {語句塊}

其中,“變量<-表達式”被稱為“生成器(Generator)”,該處的變量不需要關鍵字var或val進行聲明,其類型為后面的表達式對應的容器中的元素類型,每一次枚舉,變量就被容器中的一個新元素所初始化。例如:

scala> for (i <- 1 to 3) println(i)1

2

3

其中,1 to 3為一個整數的Range型容器(將在“2.2.4 數據結構”中介紹Range),包含1、2和3。i依次從1枚舉到3。for循環可以對任何類型的容器類進行枚舉,例如:

scala> for (i <- Array(3,5,6)) println(i)

3

5

6

其中,Array(3,5,6)創建了一個數組(將在“2.2.4 數據結構”中進一步介紹),for循環依次對數組的3個元素進行了枚舉。可以發現,通過這種方式遍歷一個數組,比Java語言的表達方法更加簡潔高效,而且不需要考慮索引是從0還是1開始,也不會發生數組越界問題。

for循環不僅僅可以對一個集合進行完全枚舉,還可以通過添加過濾條件對某一個子集進行枚舉,這些過濾條件被稱為“守衛式(Guard)”,基本語法結構為:

for (變量 <- 表達式 if 條件表達式) 語句塊

此時,只有當變量取值滿足if后面的條件表達式時,語句塊才被執行。例如:

scala> for (i <- 1 to 5 if i%2==0) println(i)

2

4

上面語句執行時,只輸出1到5之間能被2整除的數。如果需要添加多個過濾條件,可以增加多個if語句,并用分號隔開。從功能上講,上述語句等同于:

for (i <- 1 to 5)

  if (i%2==0) println(i)

可以通過添加多個生成器實現嵌套的for循環,其中,每個生成器之間用分號隔開,例如:

scala> for (i <- 1 to 5; j <- 1 to 3) println(i*j)

1

2

3

2

...

其中,外循環為1到5,內循環為1到3。與單個生成器類似,在多個生成器中,每個生成器都可以通過if子句添加守衛式進行條件過濾。

以上所有的for循環都只是對枚舉值進行某些操作即結束,實際上,Scala的for結構更靈活之處體現在,可以在每次執行的時候創造一個值,然后將包含了所有產生值的容器對象作為for循環表達式的結果返回。為了做到這一點,只需要在循環體前加上yield關鍵字,即for結構為:

for (變量 <- 表達式) yield {語句塊}

其中yield后的語句塊中最后一個表達式的值作為每次循環的返回值,例如:

scala> val r=for (i <- Array(1,2,3,4,5) if i%2==0) yield { println(i); i}

2

4

r: Array[Int] = Array(2,4)

執行結束后,r為包含元素2和4的新數組。這種帶有yield關鍵字的for循環,被稱為“for推導式”。也就是說,通過for循環遍歷一個或多個集合,對集合中的元素進行“推導”,從而計算得到新的集合,用于后續的其他處理。

4.異常處理結構

Scala不支持Java中的檢查型異常(Checked Exception),將所有異常都當作非檢查型,因此,在方法聲明中不需要像Java中那樣使用throw子句。和Java一樣,Scala也使用try-catch結構來捕獲異常,例如:

import java.io.FileReader

import java.io.FileNotFoundException

import java.io.IOException

try {

  val f = new FileReader("input.txt")

  // 文件操作

} catch {

  case ex: FileNotFoundException =>... // 文件不存在時的操作

  case ex: IOException =>... // 發生I/O錯誤時的操作

} finally {

  file.close() // 確保關閉文件

}

如果try程序體正常被執行,則沒有異常拋出;反之,如果執行出錯,則拋出異常。該異常被catch子句捕獲,捕獲的異常與每個case子句中的異常類別進行比較(這里使用了模式匹配,將在“2.3.6 模式匹配”中介紹)。如果異常是FileNotFoundException,第一個case子句將被執行;如果是IOException類型,第二個 case 子句將被執行;如果都不是,那么該異常將向上層程序體拋出。其中,finally 子句不管是否發生異常,都會被執行。finally子句是可選的。與Java類似,Scala也支持使用throw關鍵字手動拋出異常。

5.對循環的控制

為了提前終止整個循環或者跳到下一個循環,Java提供了break和continue兩個關鍵字,但是, Scala 沒有提供這兩個關鍵字,而是通過一個名稱為 Breaks 的類來實現類似的功能,該類位于包scala.util.control下。Breaks類有兩個方法用于對循環結構進行控制,即breakable和break,通常都是放在一起配對使用,其基本使用方法如下:

breakable{

...

if(...) break

...

}

即將需要控制的語句塊作為參數放在breakable后面,然后,其內部在某個條件滿足時調用break方法,程序將跳出breakable方法。通過這種通用的方式,就可以實現Java循環中的break和continue功能。下面通過一個例子來說明,請在Linux系統中新建一個代碼文件TestBreak.scala,內容如下:

//代碼文件為/usr/local/scala/mycode/TestBreak.scala

import util.control.Breaks._ //導入Breaks類的所有方法

val array = Array(1,3,10,5,4)

breakable{

for(i<- array){

   if(i>5) break //跳出breakable,終止for循環,相當于Java中的break

println(i)

  }

}

// 上面的for語句將輸出1,3

for(i<- array){

  breakable{

    if(i>5) break //跳出breakable,終止當次循環,相當于Java的continue

println(i)

  }

}

// 上面的for語句將輸出1,3,5,4

可以在Scala REPL中使用“:load /usr/local/scala/mycode/TestBreak.scala”執行該代碼文件并查看程序執行效果。

2.2.4 數據結構

在Scala編程中經常需要用到各種數據結構,比如數組(Array)、元組(Tuple)、列表(List)、映射(Map)、集合(Set)等。

1.數組

數組(Array)是一種可變的、可索引的、元素具有相同類型的數據集合,它是各種高級語言中最常用的數據結構。Scala 提供了參數化類型的通用數組類 Array[T],其中,T 可以是任意的 Scala類型。Scala數組與Java數組是一一對應的。即Scala的Array[Int]可看作Java的Int[],Array[Double]可看作Java的Double[],Array[String]可看作Java的String[]??梢酝ㄟ^顯式指定類型或者通過隱式推斷來實例化一個數組,例如:

scala> val intValueArr = new Array[Int](3)

scala> val myStrArr = Array("BigData","Hadoop","Spark")

第一行通過顯式給出類型參數Int定義一個長度為3的整型數組,數組的每個元素默認初始化為0。第二行省略了數組的類型,而通過具體的3個字符串來初始化,Scala自動推斷出為字符串數組,因為Scala會選擇初始化元素的最近公共類型作為Array的參數類型。需要注意的是,第二行中沒有像Java那樣使用new關鍵字來生成一個對象,實際是因為使用了Scala中的伴生對象的apply方法,具體將在“2.3.2 對象”中介紹。

另外,不同于Java的方括號,Scala使用圓括號來訪問數組元素,索引也是從零開始。例如,對于上述定義的兩個數組,可以通過 intValueArr(0)=5改變數組元素的值,myStrArr(1)返回字符串"Hadoop"。Scala 使用圓括號而不是方括號來訪問數組元素,這里涉及到 Scala 的伴生對象的 update方法,具體將在“2.3.2 對象”中介紹。

需要注意的是,盡管兩個數組變量都用val關鍵字進行定義,但是,這只是表明這兩個變量不能再指向其他的對象,而對象本身是可以改變的,因此可以對數組內容進行改變。

既然Array[T]類是一個通用的參數化類型,那么就可以很自然地通過給定T也為Array類型來定義多維數組。Array提供了函數ofDim來定義二維和三維數組,用法如下:

val myMatrix = Array.ofDim[Int](3,4)

val myCube = Array.ofDim[String](3,2,4)

其中,第一行定義了一個3行4列的二維整型數組,如果在REPL模式下,可以看到其類型實際就是Array[Array[Int]],即它就是一個普通的數組對象,只不過該數組的元素也是數組類型。同理,第二行定義了一個三維長度分別為3、2、4的三維字符串數組,其類型實際是Array[Array[Array[String]]]。同樣可以使用多級圓括號來訪問多維數組的元素,例如myMatrix(0)(1)返回第1行第2列的元素。

2.元組

Scala的元組是對多個不同類型對象的一種簡單封裝。Scala提供了TupleN類(N的范圍為1~22),用于創建一個包含N個元素的元組。構造一個元組的語法很簡單的,只需把多個元素用逗號分開并用圓括號包圍起來就可以了。例如:

scala> val tuple = ("BigData",2015,45.0)

這里定義了包含三個元素的元組,三個元素的類型分別為String、Int和Double,因此實際上該元組的類型為Tuple3[String,Int,Double]??梢允褂孟聞澗€“_”加上從1開始的索引值,來訪問元組的元素。例如,對于剛定義的元組tuple,tuple._1的值是字符串"BigData",tuple._3的值是浮點數45.0。還可以一次性提取出元組中的元素并賦值給變量,例如,下例展示了直接提取tuple的3個元素的值,并分別賦值給三個變量(實際上這里涉及到 Scala 的模式匹配機制,將在“2.3.6 模式匹配”中進一步介紹)。

scala> val (t1,t2,t3) = tuple

t1: String = BigData

t2: Int = 2015

t3: Double = 45.0

如果需要在方法里返回多個不同類型的對象,Scala可以簡單地返回一個元組,為了實現相同的功能,Java通常需要創建一個類去封裝多個返回值。

3.容器

Scala 提供了一套豐富的容器(Collection)庫,定義了列表(List)、映射(Map)、集合(Set)等常用數據結構。根據容器中元素的組織方式和操作方式,可以區分為有序和無序、可變和不可變等不同的容器類別。Scala用了三個包來組織容器類,分別是scala.collection、scala.collection.mutable和scala.collection.immutable。從名字即可看出scala.collection.immutable包是指元素不可變的容器;scala.collection.mutable包指的是元素可變的容器;而scala.collection封裝了一些可變容器和不可變容器的超類或特質(將在“2.3.5 特質”中介紹,這里可以將其理解為Java中的接口),定義了可變容器和不可變容器的一些通用操作。scala.collection 包中的容器通常都具備對應的不可變實現和可變實現。

Scala為容器的操作精心設計了很多細粒度的特質,但對于用戶來說,無需掌握每一個特質的使用,因為Scala已經通過混入這些特質生成了各種高級的容器。圖2-4所示為scala.collection包中容器的宏觀層級關系(省略了很多細粒度的特質)。所有容器的根為Traverable特質,表示可遍歷的,它為所有的容器類定義了抽象的foreach方法,該方法用于對容器元素進行遍歷操作?;烊隩raverable特質的容器類必須給出foreach方法的具體實現。Traverable的下一級為Iterable特質,表示元素可一個個地依次迭代,該特質定義了一個抽象的iterator方法,混入該特質的容器必須實現iterator方法,返回一個迭代器(Iterator),另外,Iterable特質還給出了其從Traverable繼承的foreach方法的一個默認實現,即通過迭代器進行遍歷。

圖2-4 scala.collection包中容器的宏觀層次結構

在 Iterable下的繼承層次包括3個特質,分別是序列(Seq)、映射(Map)和集合(Set),這3種容器最大的區別是其元素的索引方式,序列是按照從0開始的整數進行索引的,映射是按照鍵值進行索引的,而集合是沒有索引的。

4.序列

序列(Sequence)是指元素可以按照特定的順序訪問的容器。在Scala的容器層級中,序列容器的根是collection.Seq特質,是對所有可變和不可變序列的抽象。序列中每個元素均帶有一個從0開始計數的固定索引位置。特質Seq具有兩個子特質LinearSeq和IndexedSeq,這兩個子特質沒有添加任何新的方法,只是針對特殊情況對部分方法進行重載,以提供更高效的實現。LinearSeq序列具有高效的head和tail操作,而IndexedSeq序列具有高效的隨機存儲操作。實現了特質LinearSeq的常用序列有列表(List)和隊列(Queue)。實現了特質IndexedSeq的常用序列有可變數組(ArrayBuffer)和向量(Vector)。

這里介紹兩種常用的序列,即列表(List)和Range。

列表

列表(List)是一種共享相同類型的不可變的對象序列,是函數式編程中最常見的數據結構。Scala的List被定義在scala.collection.immutable包中。不同于Java的java.util.List,Scala的List一旦被定義,其值就不能改變,因此,聲明List時必須初始化,例如:

scala> var strList=List("BigData","Hadoop","Spark")

上面語句定義了一個包含3個字符串的列表 strList。這里直接使用了 List,而無需加包前綴scala.collection.immutable,這是因為Scala默認導入了Predef對象,而該對象為很多常用的數據類型提供了別名定義,包括列表scala.collection.immutable.List、不可變集scala.collection.immutable.Set和不可變映射scala.collection.immutable.Map等。由于List是一個特質,因此不能直接用new關鍵字來創建一個列表,這里使用了List的apply工廠方法創建一個列表strList(關于apply方法將在“2.3.2對象”中介紹)。創建List時也可以顯示指定元素類型,例如:

scala> val l = List[Double](1,3.4)

l: List[Double] = List(1.0, 3.4)

值得注意的是,對于包括List在內的所有容器類型,如果沒有顯式指定元素類型,Scala會自動選擇所有初始值的最近公共類型來作為元素的類型。因為Scala的所有對象都來自共同的根Any,因此,原則上容器內可以容納任意不同類型的成員(盡管實際上很少這樣做),例如:

scala> val x=List(1,3.4,"Spark")

x: List[Any] = List(1, 3.4, Spark) //1,3.4,“Spark”最近公共類型為Any

列表有頭部和尾部的概念,可以分別使用head和tail方法來獲取,例如,strList.head將返回字符串"BigData",strList.tail返回List ("Hadoop","Spark"),即head返回的是列表第一個元素的值,而tail返回的是除第一個元素外的其他值構成的新列表,這體現出列表具有遞歸的鏈表結構。正是基于這一點,常用的構造列表的方法是通過在已有列表前端增加元素,使用的操作符為“::”,例如:

scala> val otherList="Apache"::strList

其中,strList是前面已經定義過的列表,執行該語句后,strList保持不變,而otherList將成為一個新的列表 List("Apache","BigData","Hadoop","Spark")。注意,這里的“::”只是 List 類型的一個方法,而且Scala規定,當方法名以冒號結尾時,其作為操作符使用時,將執行“右結合”規則,因此,"Apache"::strList等效于strList.::("Apache")。Scala還定義了一個空列表對象Nil,借助Nil可以將多個元素用操作符::串起來初始化一個列表,例如:

scala> val intList = 1::2::3::Nil

該語句與val intList = List(1,2,3)等效,注意,最后的Nil是不能省略的,因為“::”是右結合的, 3是Int型,它并沒有名為::的方法。

列表作為一種特殊的序列,可以支持索引訪問,例如,上例的 strList(1)返回字符串"Hadoop"。但是需要注意的是,由于列表采用鏈表結構,因此,除了head、tail以及其他創建新鏈表的操作是常數時間O(1),其他諸如按索引訪問的操作都需要從頭開始遍歷,因此是線性時間復雜度O(N)。為了實現所有操作都是常數時間,可以使用向量(Vector),例如:

scala> val vec1=Vector(1,2)

vec1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

scala> val vec2 = 3 +: 4 +: vec1

vec2: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2)

scala> val vec3 = vec2 :+ 5

vec3: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2, 5)

scala> vec3(3)

res6: Int = 2

上面語句中的“+:”和“:+”都是繼承自特質Seq中的方法,用于向序列的前端和尾端添加新元素,注意以“:”結尾的方法是右結合的。

List和Vector都是不可變的,其包含的對象一旦確定就不能增加和刪除。List和Vector對應的可變版本是ListBuffer和ArrayBuffer,這兩個序列都位于scala.collection.mutable中。下面以ListBuffer為例子進行說明,ArrayBuffer的使用完全類似,只是其隨機存儲效率更高。

scala> import scala.collection.mutable.ListBuffer

scala> val mutableL1 = ListBuffer(10,20,30) //初始長度為3的變長列表

mutableL1: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30)

scala> mutableL1 += 40 //在列表尾部增加一個元素40

res22: mutableL1.type = ListBuffer(10, 20, 30, 40)

scala> val mutableL2 = mutableL1:+50 //在列表尾部增加一個元素50,并返回這個新列表,原列表保持不變

mutableL2: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30, 40, 50)

scala> mutableL1.insert(2, 60,40) //從第2個索引位置開始,插入60和40

scala> mutableL1

res24: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 60, 40, 30, 40)

scala> mutableL1 -= 40 //在數組中刪除值為40的第一個元素

res25: mutableL1.type = ListBuffer(10, 20, 60, 30, 40)

scala> var temp=mutableL1.remove(2)//移除索引為2的元素,并將其返回

temp: Int = 60

scala> mutableL1

res26: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30, 40)

上述代碼中需要注意的是,“+:”方法會修改列表本身,而“+:”方法只是利用當前列表創建一個新的列表,并在其前端增加元素,當前列表本身并未改變。為了防止混淆,可以記住一個簡單的規則:對于可變序列,包含等號“=”的方法都會直接修改序列本身,否則,就是創建新序列。例如,“++=”將另一個容器中的元素添加到列表后端,而“++”執行類似操作時,則只是返回新列表,并不會修改原列表。上述規則同樣適用于下面將要介紹的可變集合和可變映射。

Range

Range類是一種特殊的、帶索引的不可變數字等差序列,其包含的值為從給定起點按一定步長增長(減?。┑街付ńK點的所有數值??梢允褂脙煞N方法創建一個 Range 對象,第一種方法是直接使用Range類的構造函數,例如:

scala> val r=new Range(1,5,1)

其中,第一個參數為起點,第二個參數為終點(終點本身不會被包含在創建得到的 Range 對象內),最后一個參數為步長。因此,上述語句創建的Range對象包括1、2、3和4共4個整數元素,可以使用從0開始的索引訪問其元素,例如,r(2)的值是3,還可以分別使用start和end成員變量訪問其起點和終點。

另一種構造Range的常用方法是使用數值類型的to方法,這種方法經常使用在for循環結構中。例如,“1 to 5”這個語句將生成一個從整數1到5的Range;如果不想包括區間終點,可以使用until方法,例如,“1 until 5”這個語句會生成1到4的Range;還可以設置非1的步長,例如,“1 to 5 by 2”這個語句將生成包含1、3和5的Range。

類似于整數的Range,還可以生成浮點值或字符型的等差序列。例如,“0.1f to 3f by 0.5f”將生成包含0.1、0.6、1.1、1.6的Range對象;“'a' to 'e' by 2”將生成包含’a'、'c'、'e’的Range對象。實際上,支持Range的類型包括Int、Long、Float、Double、Char、BigInt和BigDecimal等。

5.集合

Scala的集合(Set)是不重復元素的容器。相對于列表中的元素是按照索引順序來組織的,集合中的元素并不會記錄元素的插入順序,而是以“哈?!狈椒▽υ氐闹颠M行組織(不可變集在元素很少時會采用其他方式實現),所以,它可以支持快速找到某個元素。集合包括可變集和不可變集,分別位于scala.collection.mutable包和scala.collection.immutable包,缺省情況下創建的是不可變集。例如:

scala> var mySet = Set("Hadoop","Spark")

scala> mySet += "Scala"

其中,第一行創建集合的方法與創建數組和列表類似,通過調用Set的apply工廠方法來創建一個集合。第二行實際是一條賦值語句的簡寫形式,等效于 mySet=mySet+ "Scala",即調用了 mySet的名為+的方法,該方法返回一個新的 Set,將這個新的 Set 賦值給可變變量 mySet,因此,如果用val修飾這里的mySet,執行時將會報錯。

如果要聲明一個可變集,則需要提前引入scala.collection.mutable.Set。舉例如下:

scala> import scala.collection.mutable.Set

scala> val myMutableSet = Set("Database","BigData")

scala> myMutableSet += "Cloud Computing"

可以看出,創建可變集的方法與創建不可變集是完全一樣的。不過需要注意的是,這里創建可變集代碼的第三行與上面創建不可變集代碼的第二行,雖然看起來形式完全一樣,但是,二者有著根本的不同。回憶上節介紹的等號規則,“+=”方法會直接在原集合上添加一個元素。這里變量myMutableSet引用本身并沒有改變,因為其被val修飾,但其指向的集對象已經改變了。

6.映射

映射(Map)是一系列鍵值對的容器。在一個映射中,鍵是唯一的,但值不一定是唯一的??梢愿鶕I來對值進行快速的檢索。Scala提供了可變映射和不可變映射,分別定義在包scala.collection.mutable和scala.collection.immutable 里。默認情況下,Scala使用的是不可變映射。例如:

scala> val university = Map("XMU" ->"Xiamen University", "THU" ->"Tsinghua University","PKU"->"Peking University")

這里定義了一個從字符串到字符串的不可變映射,在 REPL 模式下,可以看到其類型為scala.collection.immutable.Map[String,String]。其中,操作符“->”是定義二元組的簡寫方式,它會返回一個包含調用者和傳入參數的二元組,在該例中,即為(String,String)類型的二元組。

如果要獲取映射中的值,可以通過鍵來獲取。對于上述實例,university("XMU")將返回字符串"Xiamen University",對于這種訪問方式,如果給定的鍵不存在,則會拋出異常,為此,訪問前可以先調用 contains 方法來確定鍵是否存在,例如,在本例中,university.contains("XMU")將返回 true,但university.contains("Fudan")將返回false。更推薦的用法是使用get方法,它會返回Option[T]類型(見2.3.3節中關于Option的示例)。

對于不可變映射,不能添加新的鍵值對,也不能修改或者刪除已有的鍵值對。對于可變映射,可以直接修改其元素。如果想使用可變映射,必須明確地導入scala.collection.mutable.Map。例如:

scala> import scala.collection.mutable.Map

scala> val university2 = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")

scala> university2("XMU") = "Ximan University"

scala> university2("FZU") = "Fuzhou University"

scala> university2 += ("TJU"->"Tianjin University")

其中,第3條語句修改了鍵為“XMU”的已有元素,第4條語句通過修改不存在的鍵“FZU”,實現了添加新元素的目的,最后一行直接調用名為“+=”的方法增加新元素。映射的兩個常用到的方法是keys和values,分別返回由鍵和值構成的容器對象。

7.迭代器

迭代器(Iterator)是一種提供了按順序訪問容器元素的數據結構。盡管構造一個迭代器與構造一個容器很類似,但迭代器并不是一個容器類,因為不能隨機訪問迭代器的元素,而只能按從前往后的順序依次訪問其元素。因此,迭代器常用于需要對容器進行一次遍歷的場景。迭代器提供了兩個基本操作:next和hasNext,可以很方便地實現對容器進行遍歷。next返回迭代器的下一個元素,并從迭代器中將該元素拋棄,hasNext用于檢測是否還有下一個元素。例如:

val iter = Iterator("Hadoop","Spark","Scala")

while (iter.hasNext) {

  println(iter.next())

}

該操作執行結束后,迭代器會移動到末尾,就不能再使用了,如果繼續執行一次println(iter.next),就會報錯,從這一點可以看出迭代器并不是一個容器,而有些類似C++中指向一個容器元素的指針,但該指針不能前后隨意移動,只能逐次向后一個元素一個元素地移動。實際上,迭代器的大部分方法都會改變迭代器的狀態,例如,調用 length 方法會返回迭代器元素的個數,但是,調用結束后,迭代器已經沒有元素了,再次進行相關操作會報錯。因此建議,除next和hasnext方法外,在對一個迭代器調用了某個方法后,不要再次使用該迭代器。

主站蜘蛛池模板: 化州市| 榆中县| 绥江县| 额济纳旗| 巩义市| 金昌市| 和田县| 高州市| 连南| 开阳县| 吴忠市| 丹东市| 水城县| 泽州县| 通山县| 政和县| 深圳市| 米林县| 肃宁县| 乌拉特前旗| 宁远县| 湘潭县| 泰和县| 化隆| 石城县| 呼玛县| 西贡区| 新宁县| 静宁县| 抚州市| 安乡县| 正安县| 彰化县| 修水县| 普兰县| 大洼县| 清苑县| 剑河县| 大安市| 闽侯县| 大悟县|