面向 Java 開發人員的 Scala 指南: 面向對象的函數編程

 

面向 Java 開發人員的 Scala 指南: 面向對象的函數編程

瞭解 Scala 如何利用兩個領域的優點

developerWorks
文檔選項
將打印機的版面設置成橫向打印模式

打印本頁

將此頁作爲電子郵件發送

將此頁作爲電子郵件發送

英文原文

英文原文


級別: 初級

Ted Neward , 主管, Neward & Associates

2008 年 2 月 04 日

在 歷史上,Java™ 平臺一直屬於面向對象編程的領域,但是現在,甚至 Java 語言的堅定支持者也開始注意應用程序開發中的一種新趨勢:函數編程。在這個新的系列中,Ted Neward 介紹了 Scala,一種針對 JVM 將函數和麪向對象技術組合在一起的編程語言。在本文中,Ted 將舉例說明您爲何應該花時間學習 Scala(例如併發),並介紹如何快速從中受益。

您永遠不會忘記您的初戀!

對 於我來說,她的名字是 Tabinda (Bindi) Khan。那是一段愉快的少年時光,準確地說是在七年級。她很美麗、聰明,而最好的是,她常常因我的笨拙的笑話而樂不可支。在七年級和八年級的時間裏,我 們經常 “出去走走”(那時我們是這麼說的)。但到了九年級,我們分開了,文雅一點的說法是,她厭倦了連續兩年聽到同樣的笨拙的男孩笑話。我永遠都不會忘記她(特 別是因爲我們在高中畢業 10 週年聚會時再次相遇);但更重要的是,我將永遠不會失去這些珍貴的(也許有點言過其實)回憶。

關於本系列

Ted Neward 潛心研究 Scala 編程語言,並帶您跟他一起徜徉。在這個新的 developerWorks 系列 中,您將深入瞭解 Scala,並在實踐中看到 Scala 的語言功能。在進行相關比較時,Scala 代碼和 Java 代碼將放在一起展示,但(您將發現)Scala 中的許多內容與您在 Java 編程中發現的任何內容都沒有直接關聯,而這正是 Scala 的魅力所在! 畢竟,如果 Java 代碼可以做到的話,又何必學習 Scala 呢?

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 主頁 下載 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 代碼
                
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 不需要任何這種手動操作,稍後我將介紹。

大量的其他函數功能(比如模式匹配)已經被引入到 Scala 語言中,但是將其全部列出超出了本文的範圍。Scala 還添加許多目前 Java 編程中沒有的功能,比如操作符重載(它完全不像大多數 Java 開發人員所想象的那樣), 具有 “更高和更低類型邊界” 的泛型、視圖等。與其他功能相比,這些功能使得 Scala 在處理特定任務方面極其強大,比如處理或生成 XML。

但抽象概述並不夠:程序員喜歡看代碼,所以讓我們來看一下 Scala 可以做什麼。





回頁首


開始認識您

根據計算機科學的慣例,我們的第一個 Scala 程序將是標準的演示程序 “Hello World”:


Listing 2. Hello.Scala
                
object HelloWorld {
def main(args: Array[String]): unit = {
System.out.println("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 應用程序將允許以相同的名字同時定義 classobject

接下來,注意 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
                
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
}
}

不幸的是,這個特殊的代碼並沒有什麼功能 …… 或者甚至沒任何用處。例如,如果想要更改顯示的消息,則必須修改 oncePerSecond 方法的主體。傳統的 Java 程序員將通過爲 oncePerSecond 定義 String 參數來包含要顯示的消息。但甚至這樣也是極端受限的:其他任何週期任務(比如 ping 遠程服務器)將需要各自版本的 oncePerSecond ,這很明顯違反了 “不要重複自己” 的規則。我認爲我可以做得更好。


清單 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)
}
}

現在,事情開始變得有趣了。在清單 4 中,函數 oncePerSecond 接受一個參數,但其類型很陌生。形式上,名爲 callback 的參數接受一個函數作爲參數。只要傳入的函數不接受任何參數(以 () 指示)且無返回(由 => 指示)值(由函數值 unit 指示),就可以使用此函數。然後請注意,在循環體中,我使用 callback 來調用傳遞的參數函數對象。

幸運的是,我在程序的其他地方已經有了這樣一個函數,名爲 timeFlies 。所以,我從 main 中將其傳遞給 oncePerSecond 函數。(您還會注意到,timeFlies 使用了一個 Scala 引入的類 Console ,它的用途與 System.out 或新的 java.io.Console 類相同。這純粹是一個審美問題;System.outConsole 都可以在這裏使用。)





回頁首


匿名函數,您的函數是什麼?

現在,這個 timeFlies 函數似乎有點浪費 — 畢竟,它除了傳遞給 oncePerSecond 函數外毫無用處。所以,我根本不會正式定義它,如清單 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 中,主函數將一塊任意代碼作爲參數傳遞給 oncePerSecond ,看起來像來自 Lisp 或 Scheme 的 lambda 表達式,事實上,這是另一種閉包。這個匿名函數 再次展示了將函數當作一級公民處理的強大功能,它允許您在繼承性以外對代碼進行全新地泛化。(Strategy 模式的粉絲們可能已經開始唾沫橫飛了。)

事實上,oncePerSecond 仍然太特殊了:它具有不切實際的限制,即回調將在每秒被調用。我可以通過接受第二個參數指明調用傳遞的函數的頻率,來將其泛化,如清單 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."))
}
}

這 是函數語言中的公共主題:創建一個只做一件事情的高級抽象函數,讓它接受一個代碼塊(匿名函數)作爲參數,並從這個高級函數中調用這個代碼塊。例如,遍歷 一個對象集合。無需在 for 循環內部使用傳統的 Java 迭代器對象,而是使用一個函數庫在集合類上定義一個函數 — 通常叫做 “iter” 或 “map” — 接受一個帶單個參數(要迭代的對象)的函數。例如,上述的 Array 類具有一個函數 filter ,此函數在清單 7 中定義:


清單 7. Array.scala 的部分清單
                
class Array[A]
{
// ...
def filter (p : (A) => Boolean) : Array[A] = ... // not shown
}

清單 7 聲明 p 是一個接受由 A 指定的泛型參數的函數,然後返回一個布爾值。Scala 文檔表明 filter “返回一個由滿足謂詞 p 的數組的所有元素組成的數組”。這意味着如果我想返回我的 Hello World 程序,查找所有以字母 G 開頭的命令行參數,則可以編寫像清單 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 接受謂詞,這是一個隱式返回布爾值(startsWith() 調用的結果)的匿名函數,並使用 args 中的每個元素來調用謂詞。如果謂詞返回 true,則它將此值添加到結果數組中。遍歷了整個數組之後,它接受結果數組並將其返回,然後此數組立即用作 “foreach” 調用的來源,此調用執行的操作就像它名字的含義一樣:foreach 接受另一個函數,並將此函數應用於數組中的每個元素(在本例中,僅顯示每個元素)。

不難想象等同於上述 HelloG.scala 的 Java 是什麼樣的,而且也不難發現 Scala 版本非常簡短,也非常清晰。

分享本文 ……

 

digg 提交到 Digg
del.icio.us 發佈到 del.icio.us
Slashdot Slashdot 一下!

結束語

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 並使用本系列開始學習!

討論
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章