Spark 入門之 Scala 語言解釋及示例講解
Scala 語言衍生自 Funnel 語言。Funnel 語言嘗試將函數語言程式設計和 Petri 網結合起來,而 Scala 的預期目標是將面向物件、函數語言程式設計和強大的型別系統結合起來,同時讓人要能寫出優雅、簡潔的程式碼。本文希望通過一系列 Java 與 Scala 語言編寫的相同程式程式碼的對比,讓讀者能夠儘快地熟悉 Scala 語言。
安裝 Scala 並除錯
首先,我們需要從官方網站下載最新的 Scala 執行包,官方網站的地址是 http://www.scala-lang.org/downloads,把下載的檔案上傳到 Linux 伺服器並解壓,然後進入解壓後目錄的 bin 目錄,進入 Scala 編譯器環境,如清單 1 所示。
清單 1. 進入 Scala 編譯器
[[email protected]:4 bin]# ./scala
Welcome to Scala version 2.11.6 (OpenJDK 64-Bit Server VM, Java 1.7.0_65).
Type in expressions to have them evaluated.
Type :help for more information.
scala>;
清單 1 顯示我們使用的是 64 位作業系統,JDK1.7。
在正式講解 Scala 之前,我們先來簡單瞭解一下它。Scala 是一種解釋性語言,可以直接翻譯,如清單 2 所示,我們讓 1 加上 3,編譯器會直接輸出整型 4。
清單 2. 整數相加
scala> 1+3
res0: Int = 4
清單 2 輸出的 res0 表示變數名,Int 表示型別,輸出值是 4,注意 Scala 是強型別語言,必須定義型別,但是 Scala 會幫助您判斷資料型別。清單 2 所定義的變數 res0,我們可以直接操作它,如清單 3 所示。
清單 3. 變數乘法
scala> res0*3
res1: Int = 12
清單 3 裡面直譯器又自動輸出一個變數 res1,注意 Scala 的所有變數都是物件,接下來我們在清單 4 所示程式裡面把兩個變數相乘。
清單 4. 變數相乘
scala> res0*res1
res2: Int = 48
清單 5. 輸出文字
scala> println("hello world!")
hello world!
注意,這裡由於 println 是 Scala 預定義匯入的類,所以可以直接使用,其他非預定義的類,需要手動匯入。
如果想要像執行 Shell 檔案一樣執行 Scala 程式,可以編寫.scala 檔案,如清單 6 所示。
清單 6. Scala 程式[root@localhost:4 bin]# cat hello.scala
println("Hello, world, from a script!")
[root@localhost:4 bin]# ./scala hello.scala
Hello, world, from a script!
通過上面簡單的介紹,讀者應該可以上手寫程式碼了,我們進入 Scala 簡介章節。
Scala 簡介
Scala 是一種把面向物件和函數語言程式設計理念加入到靜態型別語言中的語言,可以把 Scala 應用在很大範圍的程式設計任務上,無論是小指令碼或是大系統都是用 Scala 實現。Scala 執行在標準的 Java 平臺上,可以與所有的 Java 庫實現無縫互動。可以用來編寫指令碼把 Java 控制元件連結在一起。
函數語言程式設計有兩種理念做指導,第一種理念是函式是第一類值。在函式式語言中,函式也是值,例如整數和字串,它們的地位相同。您可以把函式當作引數傳遞給其他函式,當作結果從函式中返回或儲存在變數裡。也可以在函式裡定義其他函式,就好像在函式裡定義整數一樣。函數語言程式設計的第二個主要理念是程式的操作符應該把輸入值對映到輸出值而不是就地修改資料。
Scala 程式會被編譯為 JVM 的位元組碼。它們的執行期效能通常與 Java 程式一致。Scala 程式碼可以呼叫 Java 方法,訪問 Java 欄位,繼承自 Java 類和實現 Java 介面。實際上,幾乎所有 Scala 程式碼都極度依賴於 Java 庫。
Scala 極度重用了 Java 型別,Scala 的 Int 型別代表了 Java 的原始整數型別 int,Float 代表了 float,Boolean 代表了 boolean,陣列被對映到 Java 陣列。Scala 同樣重用了許多標準 Java 庫型別。例如,Scala 裡的字串文字是 Java.lang.String,而丟擲的異常必須是 java.lang.Throwable 的子類。
Scala 程式設計
Scala 的語法避免了一些束縛 Java 程式的固定寫法。例如,Scala 裡的分號是可選的,且通常不寫。Scala 語法裡還有很多其他地方省略了。例如,如何寫類及建構函式,清單 7 所示分別採用 Java 和 Scala。
清單 7. 建構函式寫法//Java 程式碼
class MyClass {
private int index;
private String name;
public MyClass(int index, String name) {
this.index = index; this.name = name;
}
}
//Scala 程式碼
class MyClass(index: Int, name: String)
根據清單 7 所示程式碼,Scala 編譯器將製造有兩個私有欄位的類,一個名為 index 的 int 型別和一個叫做 name 的 String 型別,還有一個用這些變數作為引數獲得初始值的建構函式。
Scala 可以通過讓您提升您設計和使用的介面的抽象級別來幫助您管理複雜性。例如,假設您有一個 String 變數 name,您想弄清楚是否 String 包含一個大寫字元。清單 8 所示程式碼分別採用 Java 和 Scala。
清單 8. 驗證是否大寫字元// Java 程式碼
boolean nameHasUpperCase = false;
for (int i = 0; i < name.length(); ++i) {
if (Character.isUpperCase(name.charAt(i))) {
nameHasUpperCase = true; break;
}
}
//Scala 程式碼
val nameHasUpperCase = name.exists(_.isUpperCase)
清單 8 所示程式碼,Java 程式碼把字串看作迴圈中逐字元遞進的底層級實體。
Scala 有兩種型別的變數,val 和 var。val 變數的值只能初始化一次,再次賦值會發生錯誤,var 和 Java 的變數相同,可以隨時修改。val 是函數語言程式設計的風格,變數一旦賦值就不要再做修改。
清單 9 所示程式碼定義了變數並操作變數。
清單 9. Scala 變數操作scala> val message = "hellp world"
message: String = hellp world
scala> val test = "1"
test: String = 1
scala> test ="2"
<console>:8: error: reassignment to val
test ="2"
^
scala> var test1="1"
test1: String = 1
scala> test1="2"
test1: String = 2
清單 9 所示程式碼定義了變數 message、test,並對 test 重新賦值,由於 val 型別的變數是一次性的,所以丟擲錯誤。var 型別的變數可以重新賦值,並輸出新值。字串支援多行定義,按回車後自動會換行,如清單 10 所示。
清單 10.Scala 定義多行字元
scala>val multiline=
| "try multiple line"
multiline: String = try multiple line
scala> println(multiline)
try multiple line
在 Scala 裡,定義方法採用 def 標示符,示例程式碼如清單 11 所示。
清單 11. 定義方法scala> def max(x: Int, y: Int): Int = if(x < y) y else x
max: (x: Int, y: Int)Int
scala> max(3,8)
res0: Int = 8
清單 11 所示程式碼定義了方法 Max,用於比較傳入的兩個引數的大小,輸出較大值。
函式的定義用 def 開始。每個函式引數後面必須帶字首冒號的型別標註,因為 Scala 編譯器沒辦法推斷函式引數型別。清單 11 所定義的函式如圖 1 所示,分別對函式體內的每一個元素列出了用途。 圖 1. 函式定義解釋
如清單 12 所示的 Java 和 Scala 程式碼,我們定義了一個函式 greet,呼叫該函式會打印出“Good Moring!”。
//Java 程式碼
public static void main(String[] args){
JavaScala.greet();
}
public static void greet(){
System.out.println("Good Morning!");
}
//Scala 程式碼
scala> def greet()=println("Good Morning!")
greet: ()Unit
scala> greet();
Good Morning!
上例定義了 greet() 函式,編譯器迴應 greet 是函式名,空白的括號說明函式不帶引數,Unit 是 greet 的結果型別。Unit 的結果型別指的是函式沒有返回有用的值。Scala 的 Unit 型別接近於 Java 的 void 型別,而且實際上 Java 裡每一個返回 void 的方法都被對映為 Scala 裡返回 Unit 的方法。
注意,離開 Scala 編譯器可以用:quit 或:q 命令。
與 Java 一樣, 可以通過 Scala 的名為 args 的陣列獲得傳遞給 Scala 指令碼的命令列引數。Scala 裡,陣列以零開始,通過在括號裡指定索引訪問一個元素。所以 Scala 裡陣列 steps 的第一個元素是 steps(0),而不是 Java 裡的 steps[0]。清單 13 所示程式碼編寫了一個 Scala 檔案,定義讀入第一個引數。
清單 13.定義 Main 函式引數
[[email protected]:4 bin]# ./scala hello.scala zhoumingyao
Hello, world, from a script!zhoumingyao
[[email protected]:4 bin]# cat hello.scala
println("Hello, world, from a script!"+args(0))
//Java 程式碼
System.out.println("Hello, world, from a script!"+args(0));
當我們需要執行迴圈的時候,While 是一個不錯的選擇。清單 14 所示是 While 的實現。
清單 14. While 迴圈[root@localhost:4 bin]# cat hello.scala
var i = 0;
while(i < args.length){
println(args(i))
i += 1
}
[root@localhost:4 bin]# ./scala hello.scala hello world ! this is zhoumingyao
hello
world
!
this
is
zhoumingyao
上面的 While 迴圈讀取輸入的引數,直到引數讀取完畢。
Scala 裡可以使用 new 例項化物件或類例項,通過把加在括號裡的物件傳遞給例項的構造器的方式來用值引數化例項。例如,清單 15 所示程式碼的 Scala 程式例項化 java.math.BigInteger,例項化字串陣列。
清單 15. 例項化引數val big = new java.math.BigInteger("12345")
val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
for(i <- 0 to 2)
print(greetStrings(i))
[root@localhost:4 bin]# ./scala hello.scala
Hello, world!
從技術上講,Scala 沒有操作符過載,因為它根本沒有傳統意義上的操作符。取而代之的是,諸如+,-,*和/這樣的字元可以用來做方法名。
Scala 的 List 是不可變物件序列。List[String] 包含的僅僅是 String。Scala 的 List、Scala.List。不同於 Java 的 java.util.List,總是不可變的,在 Java 中是可變的。
val oneTwoThree = List(1, 2, 3)
List“:::”的方法實現疊加功能,程式如清單 16 所示。
清單 16. 疊加方式程式碼[[email protected]:4 bin]# cat hello.scala
val oneList = List(1,2)
val twoList = List(3,4)
val combinedList = oneList ::: twoList
println(oneList + " and " + twoList +" and " + combinedList)
[[email protected]:4 bin]# ./scala hello.scala
List(1, 2) and List(3, 4) and List(1, 2, 3, 4)
初始化新 List 的方法是把所有元素用 cons 操作符串聯起來,Nil 作為最後一個元素。
清單 17. Nil 方式val oneTwoThree = 1 :: 2 :: 3 :: Nil
println(oneTwoThree)
[root@localhost:4 bin]# ./scala hello*
List(1, 2, 3)
表格 1. 方法列表
方法名 | 方法作用 |
---|---|
List() 或 Nil | 空 List |
List(“Cool”, “tools”, “rule) | 建立帶有三個值”Cool”,”tools”和”rule”的新 List[String] |
val thrill = “Will”::”fill”::”until”::Nil | 建立帶有三個值”Will”,”fill”和”until”的新 List[String] |
List(“a”, “b”) ::: List(“c”, “d”) | 疊加兩個列表(返回帶”a”,”b”,”c”和”d”的新 List[String]) |
thrill(2) | 返回在 thrill 列表上索引為 2(基於 0)的元素(返回”until”) |
thrill.count(s => s.length == 4) | 計算長度為 4 的 String 元素個數(返回 2) |
thrill.drop(2) | 返回去掉前 2 個元素的 thrill 列表(返回 List(“until”)) |
thrill.dropRight(2) | 返回去掉後 2 個元素的 thrill 列表(返回 List(“Will”)) |
thrill.exists(s => s == “until”) | 判斷是否有值為”until”的字串元素在 thrill 裡(返回 true) |
thrill.filter(s => s.length == 4) | 依次返回所有長度為 4 的元素組成的列表(返回 List(“Will”, “fill”)) |
thrill.forall(s => s.endsWith(“1”)) | 辨別是否 thrill 列表裡所有元素都以”l”結尾(返回 true) |
thrill.foreach(s => print(s)) | 對 thrill 列表每個字串執行 print 語句(”Willfilluntil”) |
thrill.foreach(print) | 與前相同,不過更簡潔(同上) |
thrill.head | 返回 thrill 列表的第一個元素(返回”Will”) |
thrill.init | 返回 thrill 列表除最後一個以外其他元素組成的列表(返回 List(“Will”, “fill”)) |
thrill.isEmpty | 說明 thrill 列表是否為空(返回 false) |
thrill.last | 返回 thrill 列表的最後一個元素(返回”until”) |
thrill.length | 返回 thrill 列表的元素數量(返回 3) |
thrill.map(s => s + “y”) | 返回由 thrill 列表裡每一個 String 元素都加了”y”構成的列表(返回 List(“Willy”, “filly”, |
thrill.mkString(“, “) | 用列表的元素建立字串(返回”will, fill, until”) |
thrill.remove(s => s.length == 4) | 返回去除了 thrill 列表中長度為 4 的元素後依次排列的元素列表(返回 List(“until”)) |
thrill.reverse | 返回含有 thrill 列表的逆序元素的列表(返回 List(“until”, “fill”, “Will”)) |
thrill.sort((s, t) => s.charAt(0).toLowerCase < | 返回包括 thrill 列表所有元素,並且第一個字元小寫按照字母順序排列的列表(返回 List(“fill”, “until”, |
thrill.tail | 返回除掉第一個元素的 thrill 列表(返回 List(“fill”, “until”)) |
另一種容器物件是元組 (tuple),與列表一樣,元組是不可變得。但與列表不同,元組可以包含不同型別的元素。元組的用處,如果您需要在方法裡返回多個物件。例項化一個裝有一些物件的新元組,只要把這些物件放在括號裡,並用逗號分隔即可。一旦例項化一個元組,可以用點號、下劃線和一個基於 1 的元素索引訪問它。如清單 18 所示。
清單 18. 元祖程式碼
val pair = (99, "Luftballons","whawo")
println(pair._1)
println(pair._2)
println(pair._3)
[[email protected]:4 bin]# ./scala hello.scala
99
Luftballons
Whawo
清單 19. Set 操作程式碼
import scala.collection.mutable.Set
val movieSet = Set("Hitch", "Poltergeist")
movieSet += "Shrek"
println(movieSet)
import scala.collection.immutable.HashSet
val hashSet = HashSet("Tomatoes", "Chilies")
println(hashSet + "Coriander")
[[email protected]:4 bin]# ./scala hello.scala
Set(Poltergeist, Shrek, Hitch)
Set(Chilies, Tomatoes, Coriander)
Map 是 Scala 裡另一種有用的集合類。Map 的類繼承機制看上去和 Set 的很像。scala.collection 包裡面有一個基礎 Map 特質和兩個子特質 Map:可變的 Map 在 scala.collection.mutable 裡,不可變的在 scala.collection.immutable 裡。
清單 19. Map 操作程式碼
import scala.collection.mutable.Map
val treasureMap = Map[Int, String]()
treasureMap += (1 -> "Go to island.")
treasureMap += (2 -> "Find big X on ground.")
treasureMap += (3 -> "Dig.")
println(treasureMap(2))
輸出:
Find big X on ground.
如果我們嘗試從檔案按行讀取內容,程式碼可以如清單 20 所示。
清單 20. 讀取檔案[[email protected]:2 bin]# cat hello.scala
import scala.io.Source
if (args.length > 0) {
for (line <- Source.fromFile(args(0)).getLines)
println(line.length + " " + line)
} else
Console.err.println("Please enter filename")
執行命令: [[email protected]:2 bin]# ./scala hello.scala hello.scala
輸出結果:
23 import scala.io.Source
23 if (args.length > 0) {
52 for (line <- Source.fromFile(args(0)).getLines)
42 println(line.length + " " + line)
7 } else
48 Console.err.println("Please enter filename")
清單 20 所示指令碼從 scala.io 引入類 Source,然後檢查是否命令列裡定義了至少一個引數。表示式 Source.fromFile(args(0)),嘗試開啟指定的檔案並返回一個 Source 物件。函式返回 Iterator[String],在每個列舉裡提供一行包含行結束符的資訊。
類是物件的藍圖我們在很多場合都需要使用類,舉例來說,您定義了 ChecksumAccumulator 類並給它一個叫做 sum 的 var 欄位,然後再例項化兩次。程式碼如清單 21 所示。
清單 21. 類的定義和使用
class ChecksumAccumulator { var sum = 0 }
val acc = new ChecksumAccumulator val csa = new ChecksumAccumulator
注意,Public 是 Scala 的預設訪問級別,Scala 比 Java 更面向物件的一個方面是 Scala 沒有靜態成員。替代品是 Scala 有單例物件 Singleton object。除了用 object 關鍵字替換了 class 關鍵字以外,單例物件的定義看上去就像是類定義,清單 22 是單例物件的定義方法,Java 類似程式碼請見已釋出的本文作者編寫的《設計模式第一部分:單例模式》一文。
清單 22. 單例方法
import scala.collection.mutable.Map
object ChecksumAccumulator {
private val cache = Map[String, Int]()
def calculate(s: String): Int =
if (cache.contains(s))//檢查快取是否存在
cache(s)//快取存在,直接返回對映裡面的值
else {//快取不存在
val acc = new ChecksumAccumulator
for (c <- s)//對傳入字串的每個字元迴圈一次
acc.add(c.toByte)//字元轉換成 Byte
val cs = acc.checksum()
cache += (s -> cs)
cs
}
}
上面的單例物件有一個 calculate 方法,用來計算所帶的 String 引數中字元的校驗和。它還有一個私有欄位 Cache,一個快取之前計算過的校驗和的可變對映。這裡我們使用了快取例子來說明帶有域的單例物件。像這樣的快取是通過記憶體換計算時間的方式做到效能的優化。通常意義上說,只有遇到了快取能解決的效能問題時,才可能用到這樣的例子,而且應該使用弱對映(weak map),如 scala.Collection.jcl 的 WeakHashMap,這樣如果記憶體稀缺的話,快取裡的條目就會被垃圾回收機制回收掉。
要執行 Scala 程式,一定要提供一個有 main 方法的孤立單例物件名,main 方法帶有一個 Array[String] 的引數,結果型別為 Unit。任何擁有合適簽名的 main 方法的單例物件都可以用來作為程式的入口點。
清單 23. main 函式 import ChecksumAccumulator.calculate
object Summer {
def main(args: Array[String]) {
for (arg <- args)
println(arg + ": " + calculate(arg))
}
Scala 隱式引用了包 java.lang、scala 的成員、Predef 的單例物件。Predef 包括 println 和 assert 等等。清單 21 和 23 所示程式碼裡,無論 ChecksumAccumulator.scala 還是 Summer.scala 都不是指令碼,因為他們是以定義結束的。反過來說,指令碼必然以一個結果表示式結束。因此如果您嘗試以指令碼方式執行 Summer.scala,Scala 直譯器將會報錯說 Summer.scala 不是以結果表示式結束的(當然前提是您沒有在 Summer 物件定義之後加上任何您自己的表示式)。正確的做法是,您需要用 Scala 編譯器真正地編譯這些檔案,然後執行輸出的類檔案。其中一種方式是使用 scalac,Scala 的基本編譯器。輸入$ scalac ChecksumAccumulator.scala Summer.scala 命令會編譯您的原始碼,每次編譯器啟動時,都要花一些時間掃描 jar 檔案內容,並在即使您提交的是新的原始檔也需要檢視之前完成其他初始化工作。
因此,Scala 的釋出包裡還包括了一個叫做 fsc(快速 Scala 編譯器)的 Scala 編譯器後臺服務:daemon。您可以這樣使用: $ fsc ChecksumAccumulator.scala Summer.scala 第一次執行 fsc 時,會建立一個繫結在您計算機埠上的本地伺服器後臺程序。然後它就會把檔案列表通過埠傳送給後臺程序去編譯,後臺程序完成編譯。下一次您執行 fsc 時,後臺程序就已經在運行了,於是 fsc 將只是把檔案列表發給後臺程序,它會立刻開始編譯檔案。使用 fsc,您只需要在第一次等待 Java 執行時環境的啟動。如果想停止 fsc 後臺程序,可以執行 fsc -shutdown 來關閉。
不論執行 scalac 還是 fsc 命令,都將建立 Java 類檔案,然後您可以用 Scala 命令,就像之前的例子裡呼叫直譯器那樣執行它。不過,不是像前面每個例子裡那樣把包含了 Scala 程式碼的帶有.scala 副檔名的檔案交給它解釋執行,而是採用這樣的方式,$ scala Summer of love。
本文對 Scala 語言的基礎做了一些解釋,由於篇幅所限,所以下一篇文章裡會針對 Spark 附帶的示例程式碼、Spark 原始碼中出現的 Scala 程式碼進行解釋。
結束語
通過本文的學習,讀者瞭解瞭如何下載、部署 Scala。此外,通過編寫 Scala 與 Java 相同功能的程式,讓 Java 程式設計師可以快速掌握 Scala 語言,為後面的 Spark 原始碼分析文章做知識準備。目前市面上釋出的 Spark 中文書籍對於初學者來說大多較為難讀懂,作者力求推出一系列 Spark 文章,讓讀者能夠從實際入手的角度來了解 Spark。後續除了應用之外的文章,還會致力於基於 Spark 的系統架構、原始碼解釋等方面的文章釋出。