程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 面向Java開發人員的Scala指南 - 面向對象的函數編程

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

編輯:關於JAVA

在歷史上,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 主頁 下載 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 應用程序將允許以相同的名字同時定義 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

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.out 或 Console 都可以在這裡使用。)

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

現在,這個 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 版本非常簡短,也非常清晰。

結束語

Scala 中的編程如此地熟悉,同時又如此地不同。相似之處在於您可以使用已經了解而且鐘愛多年的相同的核心 Java 對象,但明顯不同的是考慮將程序分解成部分的方式。在面向 Java 開發人員的 Scala 指南 的第一篇文章中,我僅僅簡單介紹了 Scala 的功能。將來還有很多內容尚待挖掘,但是現在讓我們陶醉在函數化的過程中吧!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved