面向 Java 開發人員的 Scala 指南: 面向對象的函數編程瞭解 Scala 如何利用兩個領域的優點 |
|
級別: 初級
Ted Neward , 主管, Neward & Associates
2008 年 2 月 04 日
在 歷史上,Java™ 平臺一直屬於面向對象編程的領域,但是現在,甚至 Java 語言的堅定支持者也開始注意應用程序開發中的一種新趨勢:函數編程。在這個新的系列中,Ted Neward 介紹了 Scala,一種針對 JVM 將函數和麪向對象技術組合在一起的編程語言。在本文中,Ted 將舉例說明您爲何應該花時間學習 Scala(例如併發),並介紹如何快速從中受益。
您永遠不會忘記您的初戀!
對 於我來說,她的名字是 Tabinda (Bindi) Khan。那是一段愉快的少年時光,準確地說是在七年級。她很美麗、聰明,而最好的是,她常常因我的笨拙的笑話而樂不可支。在七年級和八年級的時間裏,我 們經常 “出去走走”(那時我們是這麼說的)。但到了九年級,我們分開了,文雅一點的說法是,她厭倦了連續兩年聽到同樣的笨拙的男孩笑話。我永遠都不會忘記她(特 別是因爲我們在高中畢業 10 週年聚會時再次相遇);但更重要的是,我將永遠不會失去這些珍貴的(也許有點言過其實)回憶。
|
Java 編程和麪向對象是許多程序員的 “初戀”,我們對待它就像對待 Bindi 一樣尊重和完全的愛慕。一些開發人員會告訴您 Java 將他們從內存管理和 C++ 的煉獄中解救出來了。其他一些人會告訴您 Java 編程使他們擺脫了對過程性編程的絕望。甚至對於一些開發人員來說,Java 代碼中的面向對象編程就是 “他們做事情的方式”。(嘿嘿,如果這對我爸爸,以及爺爺有用該多好!)
然而,時間最終會沖淡所有對初戀的記 憶,生活仍然在繼續。感情已經變了,故事中的主角也成熟了(並且學會了一些新笑話)。但最重要的是,我們周圍的世界變了。許多 Java 開發人員意識到儘管我們深愛 Java 編程,但也應該抓住開發領域中的新機會,並瞭解如何利用它們。
在最近五年中,對 Java 語言的不滿情緒逐漸增多。儘管一些人可能認爲 Ruby on Rails 的發展是主要因素,但是我要爭辯的是,RoR(被稱爲 Ruby 專家 )只是結果,而非原因。或者,可以更準確地說,Java 開發人員使用 Ruby 有着更深刻、更隱伏的原因。
簡單地說,Java 編程略顯老態了。
或者,更準確地說,Java 語言 略顯老態了。
考 慮一下:當 Java 語言最初誕生時,Clinton(第一位)在辦公室中,很少有人使用 Internet,這主要是因爲撥號是在家裏使用網絡的惟一方式。博客還沒有發明出來,每個人相信繼承是重用的基本方法。我們還相信,對象是爲對世界進行 建模的最好方法,摩爾定律將永遠統治着世界。
實際上,摩爾定律引起了行業內許多人的特別關注。自 2002/2003 年以來,微處理器技術的發展使得具有多個 “內核” 的 CPU 得以創造出來:本質上是一個芯片內具有多個 CPU。這違背了摩爾定律,摩爾定律認爲 CPU 速度將每隔 18 個月翻一倍。在兩個 CPU 上同時執行多線程環境,而不是在單個 CPU 上執行標準循環週期,這意味着代碼必須具有牢固的線程安全性,才能存活下來。
學術界已經展開了圍繞此問題的許 多研究,導致了過多新語言的出現。關鍵問題在於許多語言建立在自己的虛擬機或解釋器上,所以它們代表(就像 Ruby 一樣)到新平臺的轉換。併發衝突是真正的問題所在,一些新語言提供了強大的解決方案,太多的公司和企業對 10 年前從 C++ 到 Java 平臺的遷移仍記憶猶新。許多公司都不願意冒遷移到新平臺的風險。事實上,許多公司對上一次遷移到 Java 平臺仍心有餘悸。
瞭解 Scala。
|
Scala 是一種函數對象混合的語言,具有一些強大的優點:
- 首先,Scala 可編譯爲 Java 字節碼,這意味着它在 JVM 上運行。除了允許繼續利用豐富的 Java 開源生態系統之外,Scala 還可以集成到現有的 IT 環境中,無需進行遷移。
- 其次,Scala 基於 Haskell 和 ML 的函數原則,大量借鑑了 Java 程序員鍾愛的面向對象概念。因此,它可以將兩個領域的優勢混合在一起,從而提供了顯著的優點,而且不會失去我們一直依賴的熟悉的技術。
- 最 後,Scala 由 Martin Odersky 開發,他可能是 Java 社區中研究 Pizza 和 GJ 語言的最著名的人,GJ 是 Java 5 泛型的工作原型。而且,它給人一種 “嚴肅” 的感覺;該語言並不是一時興起而創建的,它也不會以同樣的方式被拋棄。
Scala 的名稱表明,它還是一種高度可伸縮 的語言。我將在本系列的後續文章中介紹有關這一特性的更多信息。
可以從 Scala 主頁 下載 Scala 包。截止到撰寫本文時,最新的發行版是 2.6.1-final。它可以在 Java 安裝程序版本 RPM 和 Debian 軟件包 gzip/bz2/zip 包中獲得,可以簡單地將其解壓到目標目錄中,而且可以使用源碼 tarball 從頭創建。(Debian 用戶可以使用 “apt-get install” 直接從 Debian 網站上獲得 2.5.0-1 版。2.6 版本具有一些細微的差異,所以建議直接從 Scala 網站下載和安裝。)
將 Scala 安裝到所選的目標目錄中 — 我是在 Windows®
環境中撰寫本文的,所以我的目標目錄是
C:/Prg/scala-2.6.1-final。將環境變量
SCALA_HOME
定義爲此目錄,將
SCALA_HOME/bin
放置於 PATH
中以便從命令行調用。要測試安裝,從命令行提示符中激發 scalac
-version
。它應該以 Scala 版本 2.6.1-final 作爲響應。
|
開始之前,我將列出一些必要的函數概念,以幫助理解爲何 Scala 以這種方式操作和表現。如果您對函數語言 — Haskell、ML 或函數領域的新成員 F# — 比較熟悉,可以 跳到下一節 。
函 數語言的名稱源於這樣一種概念:程序行爲應該像數學函數一樣;換句話說,給定一組輸入,函數應始終返回相同的輸出。這不僅意味着每個函數必須返回一個值, 還意味着從一個調用到下一個調用,函數本質上不得具有內蘊狀態(intrinsic state)。這種無狀態的內蘊概念(在函數/對象領域中,默認情況下指的是永遠不變的對象),是函數語言被認爲是併發領域偉大的 “救世主” 的主要原因。
與許多最近開始在 Java 平臺上佔有一席之地的動態語言不同,Scala 是靜態類型的,正如 Java 代碼一樣。但是,與 Java 平臺不同,Scala 大量利用了類型推斷(type inferencing) ,這意味着,編譯器深入分析代碼以確定特定值的類型,無需編程人員干預。類型推斷需要較少的冗餘類型代碼。例如,考慮聲明本地變量併爲其賦值的 Java 代碼,如清單 1 所示:
清單 1. 聲明本地變量併爲其賦值的 Java 代碼
|
Scala 不需要任何這種手動操作,稍後我將介紹。
大量的其他函數功能(比如模式匹配)已經被引入到 Scala 語言中,但是將其全部列出超出了本文的範圍。Scala 還添加許多目前 Java 編程中沒有的功能,比如操作符重載(它完全不像大多數 Java 開發人員所想象的那樣), 具有 “更高和更低類型邊界” 的泛型、視圖等。與其他功能相比,這些功能使得 Scala 在處理特定任務方面極其強大,比如處理或生成 XML。
但抽象概述並不夠:程序員喜歡看代碼,所以讓我們來看一下 Scala 可以做什麼。
|
根據計算機科學的慣例,我們的第一個 Scala 程序將是標準的演示程序 “Hello World”:
Listing 2. Hello.Scala
|
使用 scalac Hello.scala
編譯此程序,然後使用 Scala 啓動程序(scala HelloWorld
)或使用傳統的 Java
啓動程序運行生成的代碼,注意,將 Scala 核心庫包括在 JVM 的類路徑(java -classpath %SCALA_HOME%/lib/scala-library.jar;.
HelloWorld
)中。不管使用哪一種方法,都應出現傳統的問候。
清單 2 中的一些元素對於您來說一定很熟悉,但也使用了一些新元素。例如,首先,對 System.out.println
的熟悉的調用演示了 Scala 對底層 Java 平臺的忠誠。Scala 充分利用了 Java 平臺可用於 Scala 程序的強大功能。(事實上,它甚至會允許 Scala 類型繼承 Java 類,反之亦然,但更多信息將在稍後介紹。)
另一方面,如果仔細觀察,您還會注意到,在 System.out.println
調用的結尾處缺少分號;這並非輸入錯誤。與 Java 平臺不同,如果語句很明顯是在一行的末尾終結,則 Scala
不需要分號來終結語言。但是,分號仍然受支持,而且有時候是必需的,例如,多個語句出現在同一物理行時。通常,剛剛入門的 Scala
程序員不用考慮需不需加分號,當需要分號的時候,Scala 編譯器將提醒程序員(通常使用閃爍的錯誤消息)。
此外,還有一處微小的改進,Scala 不需要包含類定義的文件來反映類的名稱。一些人將發現這是對 Java 編程的振奮人心的變革;那些沒有這樣做的人可以繼續使用 Java “類到文件” 的命名約定,而不會出現問題。
現在,看一下 Scala 從何處真正脫離傳統的 Java/面向對象代碼。
|
對於初學者,Java 發燒友將注意到,HelloWorld
是使用關鍵字 object
來定義的,而不是使用 class
。這是 Scala 對單例模式(Singleton pattern)的認可 —
object
關鍵字告訴 Scala 編譯器這將是個單例對象,因此 Scala 將確保只有一個 HelloWorld
實例存在。基於同樣的原因,注意 main
沒有像在 Java 編程中一樣被定義爲靜態方法。事實上,Scala 完全避開了 static
的使用。如果應用程序需要同時具有某個類型的實例和某種 “全局” 實例,則 Scala
應用程序將允許以相同的名字同時定義 class
和 object
。
接下來,注意 main
的定義,與 Java 代碼一樣,是 Scala 程序可接受的輸入點。它的定義,雖然看起來與 Java 的定義不同,實際上是等同的:main 接受
String
數組作爲參數且不返回任何值。但是,在 Scala 中,此定義看起來與 Java 版本稍有差異。args
參數被定義爲 args: Array[String]
。
在 Scala 中,數組表示爲泛型化的 Array
類的實例,這正是 Scala 使用方括號(“[]”)而非尖括號(“<>”)來指明參數化類型的原因。此外,爲了保持一致性,整個語言中都使用 name:
type
的這種模式。
與其他傳統函數語言一樣,Scala 要求函數(在本例中爲一個方法)必須始終返回一個值。因此,它返回稱爲 unit
的 “無值” 值。針對所有的實際目的,Java 開發人員可以將 unit
看作 void
,至少目前可以這樣認爲。
方法定義的語法似乎比較有趣,當它使用 =
操作符時,就像將隨後的方法體賦值給 main
標識符。事實上,真正發生的事情是:在函數語言中,就像變量和常量一樣,函數是一級概念,所以語法上也是一樣地處理。
|
函數作爲一級概念的一個含義是,它們必須被識別爲單獨的結構,也稱爲閉包 ,這是 Java 社區最近一直熱烈爭論的話題。在 Scala 中,這很容易完成。考慮清單 3 中的程序,此程序定義了一個函數,該函數每隔一秒調用一次另一個函數:
清單 3. Timer1.scala
|
不幸的是,這個特殊的代碼並沒有什麼功能 …… 或者甚至沒任何用處。例如,如果想要更改顯示的消息,則必須修改 oncePerSecond
方法的主體。傳統的 Java 程序員將通過爲 oncePerSecond
定義 String
參數來包含要顯示的消息。但甚至這樣也是極端受限的:其他任何週期任務(比如 ping 遠程服務器)將需要各自版本的 oncePerSecond
,這很明顯違反了 “不要重複自己” 的規則。我認爲我可以做得更好。
清單 4. Timer2.scala
|
現在,事情開始變得有趣了。在清單 4 中,函數 oncePerSecond
接受一個參數,但其類型很陌生。形式上,名爲 callback
的參數接受一個函數作爲參數。只要傳入的函數不接受任何參數(以 () 指示)且無返回(由 => 指示)值(由函數值 unit 指示),就可以使用此函數。然後請注意,在循環體中,我使用 callback
來調用傳遞的參數函數對象。
幸運的是,我在程序的其他地方已經有了這樣一個函數,名爲 timeFlies
。所以,我從 main
中將其傳遞給 oncePerSecond
函數。(您還會注意到,timeFlies
使用了一個 Scala 引入的類 Console
,它的用途與 System.out
或新的 java.io.Console
類相同。這純粹是一個審美問題;System.out
或 Console
都可以在這裏使用。)
|
現在,這個 timeFlies
函數似乎有點浪費 — 畢竟,它除了傳遞給 oncePerSecond
函數外毫無用處。所以,我根本不會正式定義它,如清單 5 所示:
清單 5. Timer3.scala
|
在清單 5 中,主函數將一塊任意代碼作爲參數傳遞給 oncePerSecond
,看起來像來自 Lisp 或 Scheme 的 lambda
表達式,事實上,這是另一種閉包。這個匿名函數
再次展示了將函數當作一級公民處理的強大功能,它允許您在繼承性以外對代碼進行全新地泛化。(Strategy 模式的粉絲們可能已經開始唾沫橫飛了。)
事實上,oncePerSecond
仍然太特殊了:它具有不切實際的限制,即回調將在每秒被調用。我可以通過接受第二個參數指明調用傳遞的函數的頻率,來將其泛化,如清單 6 所示:
清單 6. Timer4.scala
|
這
是函數語言中的公共主題:創建一個只做一件事情的高級抽象函數,讓它接受一個代碼塊(匿名函數)作爲參數,並從這個高級函數中調用這個代碼塊。例如,遍歷
一個對象集合。無需在 for 循環內部使用傳統的 Java 迭代器對象,而是使用一個函數庫在集合類上定義一個函數 — 通常叫做 “iter”
或 “map” — 接受一個帶單個參數(要迭代的對象)的函數。例如,上述的 Array
類具有一個函數 filter
,此函數在清單 7 中定義:
清單 7. Array.scala 的部分清單
|
清單 7 聲明 p
是一個接受由 A
指定的泛型參數的函數,然後返回一個布爾值。Scala 文檔表明 filter
“返回一個由滿足謂詞 p 的數組的所有元素組成的數組”。這意味着如果我想返回我的 Hello World 程序,查找所有以字母 G 開頭的命令行參數,則可以編寫像清單 8 一樣簡單的代碼:
清單 8. Hello, G-men!
|
此處,filter
接受謂詞,這是一個隱式返回布爾值(startsWith()
調用的結果)的匿名函數,並使用 args
中的每個元素來調用謂詞。如果謂詞返回 true,則它將此值添加到結果數組中。遍歷了整個數組之後,它接受結果數組並將其返回,然後此數組立即用作 “foreach” 調用的來源,此調用執行的操作就像它名字的含義一樣:foreach
接受另一個函數,並將此函數應用於數組中的每個元素(在本例中,僅顯示每個元素)。
不難想象等同於上述 HelloG.scala
的 Java 是什麼樣的,而且也不難發現 Scala 版本非常簡短,也非常清晰。
|
Scala 中的編程如此地熟悉,同時又如此地不同。相似之處在於您可以使用已經瞭解而且鍾愛多年的相同的核心 Java 對象,但明顯不同的是考慮將程序分解成部分的方式。在面向 Java 開發人員的 Scala 指南 的第一篇文章中,我僅僅簡單介紹了 Scala 的功能。將來還有很多內容尚待挖掘,但是現在讓我們陶醉在函數化的過程中吧!
學習
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文
。
- “Java EE 迎合 Web 2.0
”
(Constantine Plotnikov、Artem Papkov、Jim Smith; developerWorks,2007 年 11
月):指出與 Web 2.0 不兼容的 Java EE 平臺的原則,並介紹彌合此裂縫的技術,其中包括 Scala。
- “
Java 理論和實踐
:應用 fork-join 框架
”(Brian Goetz,developerWorks,2007 年 11 月):fork-join 抽象提供了一種自然的基於 Java 機制,來分解算法以有效利用硬件並行性。
- “Java 語言中的函數編程
”(Abhijit Belapurkar,developerWorks,2004 年 7 月):從 Java 開發人員角度解釋函數編程的優點和用法。
-
Programming in Scala
(Martin Odersky、Lex Spoon 和 Bill Venners;Artima,2007 年 12 月):第一部介紹 Scala 的圖書,Scala 創始人 Martin Odersky 參與撰寫。
-
developerWorks Java 技術專區
:有關 Java 編程方方面面的數百篇文章。
獲得產品和技術
- Scala 主頁 :下載 Scala 並使用本系列開始學習!
討論