從Java走進(jìn)Scala:面向?qū)ο蟮暮瘮?shù)編程
您永遠(yuǎn)不會(huì)忘記您的初戀!
對于我來說,她的名字是 Tabinda (Bindi) Khan。那是一段愉快的少年時(shí)光,準(zhǔn)確地說是在七年級。她很美麗、聰明,而最好的是,她常常因我的笨拙的笑話而樂不可支。在七年級和八年級的時(shí)間里,我們經(jīng)常 “出去走走”(那時(shí)我們是這么說的)。但到了九年級,我們分開了,文雅一點(diǎn)的說法是,她厭倦了連續(xù)兩年聽到同樣的笨拙的男孩笑話。我永遠(yuǎn)都不會(huì)忘記她(特別是因?yàn)槲覀冊诟咧挟厴I(yè) 10 周年聚會(huì)時(shí)再次相遇);但更重要的是,我將永遠(yuǎn)不會(huì)失去這些珍貴的(也許有點(diǎn)言過其實(shí))回憶。
Java 編程和面向?qū)ο笫窃S多程序員的 “初戀”,我們對待它就像對待 Bindi 一樣尊重和完全的愛慕。一些開發(fā)人員會(huì)告訴您 Java 將他們從內(nèi)存管理和 C++ 的煉獄中解救出來了。其他一些人會(huì)告訴您 Java 編程使他們擺脫了對過程性編程的絕望。甚至對于一些開發(fā)人員來說,Java 代碼中的面向?qū)ο缶幊叹褪?“他們做事情的方式”。(嘿嘿,如果這對我爸爸,以及爺爺有用該多好!)
然而,時(shí)間最終會(huì)沖淡所有對初戀的記憶,生活仍然在繼續(xù)。感情已經(jīng)變了,故事中的主角也成熟了(并且學(xué)會(huì)了一些新笑話)。但最重要的是,我們周圍的世界變了。許多 Java 開發(fā)人員意識到盡管我們深愛 Java 編程,但也應(yīng)該抓住開發(fā)領(lǐng)域中的新機(jī)會(huì),并了解如何利用它們。
我將始終愛著你 ……
在最近五年中,對 Java 語言的不滿情緒逐漸增多。盡管一些人可能認(rèn)為 Ruby on Rails 的發(fā)展是主要因素,但是我要爭辯的是,RoR(被稱為 Ruby 專家)只是結(jié)果,而非原因?;蛘?,可以更準(zhǔn)確地說,Java 開發(fā)人員使用 Ruby 有著更深刻、更隱伏的原因。
簡單地說,Java 編程略顯老態(tài)了。
或者,更準(zhǔn)確地說,Java 語言 略顯老態(tài)了。
考慮一下:當(dāng) Java 語言最初誕生時(shí),Clinton(第一位)在辦公室中,很少有人使用 Internet,這主要是因?yàn)閾芴柺窃诩依锸褂镁W(wǎng)絡(luò)的惟一方式。博客還沒有發(fā)明出來,每個(gè)人相信繼承是重用的基本方法。我們還相信,對象是為對世界進(jìn)行建模的最好方法,摩爾定律將永遠(yuǎn)統(tǒng)治著世界。
實(shí)際上,摩爾定律引起了行業(yè)內(nèi)許多人的特別關(guān)注。自 2002/2003 年以來,微處理器技術(shù)的發(fā)展使得具有多個(gè) “內(nèi)核” 的 CPU 得以創(chuàng)造出來:本質(zhì)上是一個(gè)芯片內(nèi)具有多個(gè) CPU。這違背了摩爾定律,摩爾定律認(rèn)為 CPU 速度將每隔 18 個(gè)月翻一倍。在兩個(gè) CPU 上同時(shí)執(zhí)行多線程環(huán)境,而不是在單個(gè) CPU 上執(zhí)行標(biāo)準(zhǔn)循環(huán)周期,這意味著代碼必須具有牢固的線程安全性,才能存活下來。
學(xué)術(shù)界已經(jīng)展開了圍繞此問題的許多研究,導(dǎo)致了過多新語言的出現(xiàn)。關(guān)鍵問題在于許多語言建立在自己的虛擬機(jī)或解釋器上,所以它們代表(就像 Ruby 一樣)到新平臺的轉(zhuǎn)換。并發(fā)沖突是真正的問題所在,一些新語言提供了強(qiáng)大的解決方案,太多的公司和企業(yè)對 10 年前從 C++ 到 Java 平臺的遷移仍記憶猶新。許多公司都不愿意冒遷移到新平臺的風(fēng)險(xiǎn)。事實(shí)上,許多公司對上一次遷移到 Java 平臺仍心有余悸。
了解 Scala。
一種可伸縮語言
Scala 是一種函數(shù)對象混合的語言,具有一些強(qiáng)大的優(yōu)點(diǎn):
首先,Scala 可編譯為 Java 字節(jié)碼,這意味著它在 JVM 上運(yùn)行。除了允許繼續(xù)利用豐富的 Java 開源生態(tài)系統(tǒng)之外,Scala 還可以集成到現(xiàn)有的 IT 環(huán)境中,無需進(jìn)行遷移。
其次,Scala 基于 Haskell 和 ML 的函數(shù)原則,大量借鑒了 Java 程序員鐘愛的面向?qū)ο蟾拍?。因此,它可以將兩個(gè)領(lǐng)域的優(yōu)勢混合在一起,從而提供了顯著的優(yōu)點(diǎn),而且不會(huì)失去我們一直依賴的熟悉的技術(shù)。
最后,Scala 由 Martin Odersky 開發(fā),他可能是 Java 社區(qū)中研究 Pizza 和 GJ 語言的最著名的人,GJ 是 Java 5 泛型的工作原型。而且,它給人一種 “嚴(yán)肅” 的感覺;該語言并不是一時(shí)興起而創(chuàng)建的,它也不會(huì)以同樣的方式被拋棄。
Scala 的名稱表明,它還是一種高度可伸縮 的語言。我將在本系列的后續(xù)文章中介紹有關(guān)這一特性的更多信息。
下載并安裝 Scala
可以從 Scala 主頁 下載 Scala 包。截止到撰寫本文時(shí),最新的發(fā)行版是 2.6.1-final(51CTO編者注:現(xiàn)在已經(jīng)是2.7.5版)。它可以在 Java 安裝程序版本 RPM 和 Debian 軟件包 gzip/bz2/zip 包中獲得,可以簡單地將其解壓到目標(biāo)目錄中,而且可以使用源碼 tarball 從頭創(chuàng)建。(Debian 用戶可以使用 “apt-get install” 直接從 Debian 網(wǎng)站上獲得 2.5.0-1 版。2.6 版本具有一些細(xì)微的差異,所以建議直接從 Scala 網(wǎng)站下載和安裝。)
將 Scala 安裝到所選的目標(biāo)目錄中 — 我是在 Windows® 環(huán)境中撰寫本文的,所以我的目標(biāo)目錄是 C:/Prg/scala-2.6.1-final。將環(huán)境變量 SCALA_HOME 定義為此目錄,將 SCALA_HOME\bin 放置于 PATH 中以便從命令行調(diào)用。要測試安裝,從命令行提示符中激發(fā) scalac -version。它應(yīng)該以 Scala 版本 2.6.1-final 作為響應(yīng)。
#p#
函數(shù)概念
開始之前,我將列出一些必要的函數(shù)概念,以幫助理解為何 Scala 以這種方式操作和表現(xiàn)。如果您對函數(shù)語言 — Haskell、ML 或函數(shù)領(lǐng)域的新成員 F# — 比較熟悉,可以 跳到下一節(jié)。
函數(shù)語言的名稱源于這樣一種概念:程序行為應(yīng)該像數(shù)學(xué)函數(shù)一樣;換句話說,給定一組輸入,函數(shù)應(yīng)始終返回相同的輸出。這不僅意味著每個(gè)函數(shù)必須返回一個(gè)值,還意味著從一個(gè)調(diào)用到下一個(gè)調(diào)用,函數(shù)本質(zhì)上不得具有內(nèi)蘊(yùn)狀態(tài)(intrinsic state)。這種無狀態(tài)的內(nèi)蘊(yùn)概念(在函數(shù)/對象領(lǐng)域中,默認(rèn)情況下指的是永遠(yuǎn)不變的對象),是函數(shù)語言被認(rèn)為是并發(fā)領(lǐng)域偉大的 “救世主” 的主要原因。
與許多最近開始在 Java 平臺上占有一席之地的動(dòng)態(tài)語言不同,Scala 是靜態(tài)類型的,正如 Java 代碼一樣。但是,與 Java 平臺不同,Scala 大量利用了類型推斷(type inferencing),這意味著,編譯器深入分析代碼以確定特定值的類型,無需編程人員干預(yù)。類型推斷需要較少的冗余類型代碼。例如,考慮聲明本地變量并為其賦值的 Java 代碼,如清單 1 所示:
清單 1. 聲明本地變量并為其賦值的 Java 代碼
- class BrainDead {
- public static void main(String[] args) {
- String message = "Why does javac need to be told message is a String?" +
- "What else could it be if I'm assigning a String to it?";
- }
- }
Scala 不需要任何這種手動(dòng)操作,稍后我將介紹。
大量的其他函數(shù)功能(比如模式匹配)已經(jīng)被引入到 Scala 語言中,但是將其全部列出超出了本文的范圍。Scala 還添加許多目前 Java 編程中沒有的功能,比如操作符重載(它完全不像大多數(shù) Java 開發(fā)人員所想象的那樣), 具有 “更高和更低類型邊界” 的泛型、視圖等。與其他功能相比,這些功能使得 Scala 在處理特定任務(wù)方面極其強(qiáng)大,比如處理或生成 XML。
但抽象概述并不夠:程序員喜歡看代碼,所以讓我們來看一下 Scala 可以做什么。
開始認(rèn)識您
根據(jù)計(jì)算機(jī)科學(xué)的慣例,我們的第一個(gè) Scala 程序?qū)⑹菢?biāo)準(zhǔn)的演示程序 “Hello World”:
清單 2. Hello.Scala
- object HelloWorld {
- def main(args: Array[String]): unit = {
- System.out.println("Hello, Scala!")
- }
- }
使用 scalac Hello.scala 編譯此程序,然后使用 Scala 啟動(dòng)程序(scala HelloWorld)或使用傳統(tǒng)的 Java 啟動(dòng)程序運(yùn)行生成的代碼,注意,將 Scala 核心庫包括在 JVM 的類路徑(java -classpath %SCALA_HOME%\lib\scala-library.jar;. HelloWorld)中。不管使用哪一種方法,都應(yīng)出現(xiàn)傳統(tǒng)的問候。
清單 2 中的一些元素對于您來說一定很熟悉,但也使用了一些新元素。例如,首先,對 System.out.println 的熟悉的調(diào)用演示了 Scala 對底層 Java 平臺的忠誠。Scala 充分利用了 Java 平臺可用于 Scala 程序的強(qiáng)大功能。(事實(shí)上,它甚至?xí)试S Scala 類型繼承 Java 類,反之亦然,但更多信息將在稍后介紹。)
另一方面,如果仔細(xì)觀察,您還會(huì)注意到,在 System.out.println 調(diào)用的結(jié)尾處缺少分號;這并非輸入錯(cuò)誤。與 Java 平臺不同,如果語句很明顯是在一行的末尾終結(jié),則 Scala 不需要分號來終結(jié)語言。但是,分號仍然受支持,而且有時(shí)候是必需的,例如,多個(gè)語句出現(xiàn)在同一物理行時(shí)。通常,剛剛?cè)腴T的 Scala 程序員不用考慮需不需加分號,當(dāng)需要分號的時(shí)候,Scala 編譯器將提醒程序員(通常使用閃爍的錯(cuò)誤消息)。
此外,還有一處微小的改進(jìn),Scala 不需要包含類定義的文件來反映類的名稱。一些人將發(fā)現(xiàn)這是對 Java 編程的振奮人心的變革;那些沒有這樣做的人可以繼續(xù)使用 Java “類到文件” 的命名約定,而不會(huì)出現(xiàn)問題。
現(xiàn)在,看一下 Scala 從何處真正脫離傳統(tǒng)的 Java/面向?qū)ο蟠a。
將函數(shù)和表單最終結(jié)合起來
對于初學(xué)者,Java 發(fā)燒友將注意到,HelloWorld 是使用關(guān)鍵字 object 來定義的,而不是使用 class。這是 Scala 對單例模式(Singleton pattern)的認(rèn)可 — object 關(guān)鍵字告訴 Scala 編譯器這將是個(gè)單例對象,因此 Scala 將確保只有一個(gè) HelloWorld 實(shí)例存在?;谕瑯拥脑?,注意 main 沒有像在 Java 編程中一樣被定義為靜態(tài)方法。事實(shí)上,Scala 完全避開了 static 的使用。如果應(yīng)用程序需要同時(shí)具有某個(gè)類型的實(shí)例和某種 “全局” 實(shí)例,則 Scala 應(yīng)用程序?qū)⒃试S以相同的名字同時(shí)定義 class 和 object。
接下來,注意 main 的定義,與 Java 代碼一樣,是 Scala 程序可接受的輸入點(diǎn)。它的定義,雖然看起來與 Java 的定義不同,實(shí)際上是等同的:main 接受 String 數(shù)組作為參數(shù)且不返回任何值。但是,在 Scala 中,此定義看起來與 Java 版本稍有差異。args 參數(shù)被定義為 args: Array[String]。
在 Scala 中,數(shù)組表示為泛型化的 Array 類的實(shí)例,這正是 Scala 使用方括號(“[]”)而非尖括號(“<>”)來指明參數(shù)化類型的原因。此外,為了保持一致性,整個(gè)語言中都使用 name: type 的這種模式。
與其他傳統(tǒng)函數(shù)語言一樣,Scala 要求函數(shù)(在本例中為一個(gè)方法)必須始終返回一個(gè)值。因此,它返回稱為 unit 的 “無值” 值。針對所有的實(shí)際目的,Java 開發(fā)人員可以將 unit 看作 void,至少目前可以這樣認(rèn)為。
方法定義的語法似乎比較有趣,當(dāng)它使用 = 操作符時(shí),就像將隨后的方法體賦值給 main 標(biāo)識符。事實(shí)上,真正發(fā)生的事情是:在函數(shù)語言中,就像變量和常量一樣,函數(shù)是一級概念,所以語法上也是一樣地處理。
您說的是閉包嗎?
函數(shù)作為一級概念的一個(gè)含義是,它們必須被識別為單獨(dú)的結(jié)構(gòu),也稱為閉包,這是 Java 社區(qū)最近一直熱烈爭論的話題。在 Scala 中,這很容易完成??紤]清單 3 中的程序,此程序定義了一個(gè)函數(shù),該函數(shù)每隔一秒調(diào)用一次另一個(gè)函數(shù):
清單 3. Timer1.scala
- object Timer
- {
- def oncePerSecond(): unit =
- {
- while (true)
- {
- System.out.println("Time flies when you're having fun(ctionally)...")
- Thread.sleep(1000)
- }
- }
- def main(args: Array[String]): unit =
- {
- oncePerSecond
- }
- }
不幸的是,這個(gè)特殊的代碼并沒有什么功能 …… 或者甚至沒任何用處。例如,如果想要更改顯示的消息,則必須修改 oncePerSecond 方法的主體。傳統(tǒng)的 Java 程序員將通過為 oncePerSecond 定義 String 參數(shù)來包含要顯示的消息。但甚至這樣也是極端受限的:其他任何周期任務(wù)(比如 ping 遠(yuǎn)程服務(wù)器)將需要各自版本的 oncePerSecond,這很明顯違反了 “不要重復(fù)自己” 的規(guī)則。我認(rèn)為我可以做得更好。
清單 4. Timer2.scala
- object Timer
- {
- def oncePerSecond(callback: () => unit): unit =
- {
- while (true)
- {
- callback()
- Thread.sleep(1000)
- }
- }
- def timeFlies(): unit =
- { Console.println("Time flies when you're having fun(ctionally)..."); }
- def main(args: Array[String]): unit =
- {
- oncePerSecond(timeFlies)
- }
- }
現(xiàn)在,事情開始變得有趣了。在清單 4 中,函數(shù) oncePerSecond 接受一個(gè)參數(shù),但其類型很陌生。形式上,名為 callback 的參數(shù)接受一個(gè)函數(shù)作為參數(shù)。只要傳入的函數(shù)不接受任何參數(shù)(以 () 指示)且無返回(由 => 指示)值(由函數(shù)值 unit 指示),就可以使用此函數(shù)。然后請注意,在循環(huán)體中,我使用 callback 來調(diào)用傳遞的參數(shù)函數(shù)對象。
幸運(yùn)的是,我在程序的其他地方已經(jīng)有了這樣一個(gè)函數(shù),名為 timeFlies。所以,我從 main 中將其傳遞給 oncePerSecond 函數(shù)。(您還會(huì)注意到,timeFlies 使用了一個(gè) Scala 引入的類 Console,它的用途與 System.out 或新的 java.io.Console 類相同。這純粹是一個(gè)審美問題;System.out 或 Console 都可以在這里使用。)
#p#
匿名函數(shù),您的函數(shù)是什么?
現(xiàn)在,這個(gè) timeFlies 函數(shù)似乎有點(diǎn)浪費(fèi) — 畢竟,它除了傳遞給 oncePerSecond 函數(shù)外毫無用處。所以,我根本不會(huì)正式定義它,如清單 5 所示:
清單 5. Timer3.scala
- object Timer
- {
- def oncePerSecond(callback: () => unit): unit =
- {
- while (true)
- {
- callback()
- Thread.sleep(1000)
- }
- }
- def main(args: Array[String]): unit =
- {
- oncePerSecond(() =>
- Console.println("Time flies... oh, you get the idea."))
- }
- }
在清單 5 中,主函數(shù)將一塊任意代碼作為參數(shù)傳遞給 oncePerSecond,看起來像來自 Lisp 或 Scheme 的 lambda 表達(dá)式,事實(shí)上,這是另一種閉包。這個(gè)匿名函數(shù) 再次展示了將函數(shù)當(dāng)作一級公民處理的強(qiáng)大功能,它允許您在繼承性以外對代碼進(jìn)行全新地泛化。(Strategy 模式的粉絲們可能已經(jīng)開始唾沫橫飛了。)
事實(shí)上,oncePerSecond 仍然太特殊了:它具有不切實(shí)際的限制,即回調(diào)將在每秒被調(diào)用。我可以通過接受第二個(gè)參數(shù)指明調(diào)用傳遞的函數(shù)的頻率,來將其泛化,如清單 6 所示:
清單 6. Timer4.scala
- object Timer
- {
- def periodicCall(seconds: int, callback: () => unit): unit =
- {
- while (true)
- {
- callback()
- Thread.sleep(seconds * 1000)
- }
- }
- def main(args: Array[String]): unit =
- {
- periodicCall(1, () =>
- Console.println("Time flies... oh, you get the idea."))
- }
- }
這是函數(shù)語言中的公共主題:創(chuàng)建一個(gè)只做一件事情的高級抽象函數(shù),讓它接受一個(gè)代碼塊(匿名函數(shù))作為參數(shù),并從這個(gè)高級函數(shù)中調(diào)用這個(gè)代碼塊。例如,遍歷一個(gè)對象集合。無需在 for 循環(huán)內(nèi)部使用傳統(tǒng)的 Java 迭代器對象,而是使用一個(gè)函數(shù)庫在集合類上定義一個(gè)函數(shù) — 通常叫做 “iter” 或 “map” — 接受一個(gè)帶單個(gè)參數(shù)(要迭代的對象)的函數(shù)。例如,上述的 Array 類具有一個(gè)函數(shù) filter,此函數(shù)在清單 7 中定義:
清單 7. Array.scala 的部分清單
- class Array[A]
- {
- // ...
- def filter (p : (A) => Boolean) : Array[A] = ... // not shown
- }
清單 7 聲明 p 是一個(gè)接受由 A 指定的泛型參數(shù)的函數(shù),然后返回一個(gè)布爾值。Scala 文檔表明 filter “返回一個(gè)由滿足謂詞 p 的數(shù)組的所有元素組成的數(shù)組”。這意味著如果我想返回我的 Hello World 程序,查找所有以字母 G 開頭的命令行參數(shù),則可以編寫像清單 8 一樣簡單的代碼:
清單 8. Hello, G-men!
- object HelloWorld
- {
- def main(args: Array[String]): unit = {
- args.filter( (arg:String) => arg.startsWith("G") )
- .foreach( (arg:String) => Console.println("Found " + arg) )
- }
- }
此處,filter 接受謂詞,這是一個(gè)隱式返回布爾值(startsWith() 調(diào)用的結(jié)果)的匿名函數(shù),并使用 args 中的每個(gè)元素來調(diào)用謂詞。如果謂詞返回 true,則它將此值添加到結(jié)果數(shù)組中。遍歷了整個(gè)數(shù)組之后,它接受結(jié)果數(shù)組并將其返回,然后此數(shù)組立即用作 “foreach” 調(diào)用的來源,此調(diào)用執(zhí)行的操作就像它名字的含義一樣:foreach 接受另一個(gè)函數(shù),并將此函數(shù)應(yīng)用于數(shù)組中的每個(gè)元素(在本例中,僅顯示每個(gè)元素)。
不難想象等同于上述 HelloG.scala 的 Java 是什么樣的,而且也不難發(fā)現(xiàn) Scala 版本非常簡短,也非常清晰。
結(jié)束語
Scala 中的編程如此地熟悉,同時(shí)又如此地不同。相似之處在于您可以使用已經(jīng)了解而且鐘愛多年的相同的核心 Java 對象,但明顯不同的是考慮將程序分解成部分的方式。在面向 Java 開發(fā)人員的 Scala 指南 的第一篇文章中,我僅僅簡單介紹了 Scala 的功能。將來還有很多內(nèi)容尚待挖掘,但是現(xiàn)在讓我們陶醉在函數(shù)化的過程中吧!
【相關(guān)閱讀】