在與 Martin Fowler 共同參加的一次主題演講中,他提供了一個敏銳的觀察報告:
Java 的遺產是 平台,不是 語言。
最初的 Java 技術工程師曾做過一個了不起的決定,將語言從運行時中分離出來,最終使 200 多種語言可在 Java 平台 上運行。該基礎架構對平台保持長久活力非常關鍵,因為計算機編程語言的壽命通常很短。自 2008 年以來,每年由 Oracle 主辦的 JVM 語言峰會都會為 JVM 上替代語言的實現者提供與平台工程師公開合作的機會。
歡迎來 到 Java 下一代專欄系列。在這裡,我將簡要介紹三種現代 JVM 語言:Groovy、Scala 和 Clojure,它們將范式、設計選 擇和舒適因素進行了有趣的組合。我不打算在這裡詳細介紹每種語言;它們各自的網站上都有這方面的介紹。但語言社區網 站(主要目的是福音傳道)上沒有提供語言不適應的客觀信息或任務示例。在本系列文章中,我將進行實質性對比,幫助填 補這項空白。本文准備概述 Java 下一代語言以及學習這些語言的好處。
超越 Java
Java 語言因 Bruce Tate 在其著作 超越 Java中將其稱為 完美風暴而出名,導致 Java 出名的綜合因素包括:Web 的興起、現有 Web 技術因 各種原因產生的不適應性,以及企業多層應用開發的興起。Tate 也認為完美風暴是一系列獨立事件,而其他語言不會以同 樣方式達到同樣的高度。
Java 語言已證明其功能相當靈活,但人所共知,其語法和固有范式具有一定的局限性。 盡管 Java 語言正在進行一些看似美好的改變,但其語法根本不支持一些重要的未來目標,如函數式編程元素。但是,如果 您試圖找到一種新語言取代 Java,那您就錯了。
多語言編程
多語言編程是我在 2006 年的一片博客文章中 重新提出並推廣的一個術語,多語言編程以單一語言並不適合解決所有問題的認知為基礎的。一些語言具有更適合某些特定 問題的內在特性。例如,雖然 Swing 與 Java 一樣成熟,但開發人員發現在 Java 中編寫 Swing UI 非常麻煩,因為它要 求進行類型聲明,要求行為具有匿名內部類,並且具有其他沖突因素。使用更適合構建 UI 的語言,比如帶有 SwingBuilder的 Groovy,就會使構建 Swing 應用程序變得更容易。
JVM 上運行的語言的擴展使多語言編程的構思 更具吸引力,因為您可以在維護相同的底層字節碼和庫時將其混搭。例如,SwingBuilder不能取代 Swing;它在現有 Swing API 上進行分層。當然,長期以來,開發人員一直混合使用 JVM 以外的語言(例如,混搭使用 SQL 和 JavaScript 來實現 特定目的),這在 JVM 范圍內更加普遍。許多 ThoughtWorks 項目包含多種語言,ThoughtWorks Studios 開發的所有工具 均使用混合語言。
即使 Java 依舊是您的主要開發語言,也可以了解如何運行其他語言,以便在策略上使用它們。 Java 依然是 JVM 生態系統的重要部分,但最終人們更傾向於將它用作平台匯編語言 —一個您可以完全了解性能或滿足特 定需求的地方。
發展
20 世紀 80 年代初,在我上大學期時,我們使用稱為 Pecan Pascal 的開發環境。它 的獨特之處是可以同時在 Apple II 或 IBM PC 上運行相同的 Pascal 代碼。Pecan 工程師使用某種稱為 “字節碼” 的神 秘東西實現了這一壯舉。開發人員將他們的 Pascal 代碼編譯成這種 “字節碼”,並在為每個平台本地編寫的 “虛擬機” 上運行。多麼可怕的經歷啊!甚至對於簡單的任務而言,生成代碼也極其緩慢。當時的硬件根本無法應對這種挑戰。
發布 Pecan Pascal 之後的十年,Sun 發布了 Java,Java 使用了相同的架構,對於 20 世紀 90 年代中期的硬件 環境,運行該代碼顯得有些緊張,但最終取得了成功。Java 還增加了其他開發人員友好的特性,如自動垃圾收集。使用過 像 C++ 這樣的語言之後,我再也不想在沒有垃圾收集的語言中編寫代碼。我寧願花將時間花在更高層次上的抽象上,思考 解決復雜業務問題的方法,也不願意在內存管理等復雜的管道問題上浪費時間。
計算機語言通常不會有很長的壽命 ,原因之一是語言和平台設計的創新速度。隨著我們的平台逐漸強大,可以處理的繁重作業也就越多。例如,Groovy 的 備 忘特性(2010 年增加的特性)緩沖了函數調用結果。不需要手寫緩沖代碼,這可能會引入 bug,只需調用 memoize()方法 即可,如清單 1 所示:
清單 1. 在 Groovy 中備忘一個函數
def static sum = { number -> factorsOf(number).inject(0, {i, j -> i + j}) } def static sumOfFactors = sum.memoize()
清單 1 中,sumOfFactors方法的結果是自動緩存的。您也可以使用 另一種方法自定義緩沖行為,比如 memoizeAtLeast()和 memoizeAtMost()。Clojure 還提供了備忘功能,這對 Scala 的實 現是無足輕重的。下一代語言(以及一些 Java 框架)中的高級特性(比如備忘功能)將逐漸找到它們進入 Java 語言的方 法。Java 的下一個版本將增加高階函數,使備忘功能的實現變得更容易。通過學習下一代 Java 語言,提前了解未來 Java 特性。
Groovy、Scala 和 Clojure
Groovy 是 21 世紀的 Java 語法(濃縮咖啡,而非普通咖啡)。 Groovy 的設計目標是更新並減少 Java 語法阻力,同時支持 Java 語言中的主要范式。因此,Groovy 需要 “了解” JavaBeans 這類技術,並簡化屬性訪問。Groovy 快速合並新特性,並提供了一些重要函數特性,我將在後面幾期中重點介 紹。Groovy 在根本上依然是面向對象的命令式語言。Groovy 與 Java 的兩個主要區別是,Groovy 是 靜態而非動態類型, 而且它的元程序功能更佳。
Scala 是一種充分利用了 JVM 優勢的語言,但其語法完全進行了重新設計。Scala 是 一種強靜態類型語言(比對類型要求比較嚴格的 Java 更嚴格)支持面向對象范式和函數范式,而且更青睐於後者。例如, Scala 傾向 val聲明,並使不變的變量(類似於在 Java 中將參數標記為 final)服從於 var,這創建了人們更加熟悉的可 變變量。通過大力支持這兩種范式,Scala 為您提供了從您可能是(一名面向對象的命令式程序員)到可能應該是(一名傾 向函數式的程序員)的橋梁。
Clojure 是一種 Lisp 方言,在語法上徹底背離了其他語言。它是一種強動態類型語 言(和 Groovy 一樣),反映了專斷的設計決策。雖然 Clojure 允許您用遺留 Java 進行完整和深入的交互操作,但它並 不試圖構建與舊式范式相連的橋梁。例如,Clojure 不具備糾錯功能,並且支持面向對象進行交互操作。但是,它還支持對 象程序員所習慣的所有特性,如多態性,但它以函數方式而非面向對象的方式來實現這些特性。Clojure 圍繞一些核心工程 原理(比如 Software Transactional Memory)進行設計,它打破了舊的范式,支持新的功能。
范式
除了語 法之外,這些語言最有趣的不同之處在於類型和底層主要范式:函數式或命令式。
靜態與動態類型
編程語言 中的 靜態類型指的是顯式類型聲明,如 Java 的 int x;聲明。動態類型指的是不要求提供類型聲明信息的語言。所有語言 都是 強類型語言,這意味著您的代碼可以反映賦值後的類型。
Java 的類型系統飽受責備,因為它的靜態類型帶來 了許多不便,而且無法獲得較大收益。例如,在現行的有限類型推斷之前,Java 要求開發人員在賦值等號的兩側重復進行 類型聲明。Scala 與 Java 相比是更加靜態的類型,但在日常使用時並不麻煩,因為它很好地利用了類型推斷。
Groovy 有一個行為,乍一看,該行為似乎架起了靜態與動態之間的橋梁。請考慮清單 2 所示的簡單集合工廠:
清單 2. Groovy 集合工廠
class CollectionFactory { def List getCollection(description) { if (description == "Array-like") new ArrayList() else if (description == "Stack-like") new Stack() } }
清單 2 中的類充當了一個工廠,它將根據傳遞的 description 參數返回兩個 list 接口實現程序中的一個 (ArrayList 或 Stack)。對 Java 開發人員而言,這似乎能夠確保返回的結果滿足規則。但是,清單 3 中的這兩個單元 測試顯示了它帶來的並發症:
清單 3. 測試 Groovy 中的集合類型
@Test void test_search() { List l = f.getCollection("Stack-like") assertTrue l instanceof java.util.Stack l.push("foo") assertThat l.size(), is(1) def r = l.search("foo") } @Test(expected=groovy.lang.MissingMethodException.class) void verify_that_typing_does_not_help() { List l = f.getCollection("Array-like") assertTrue l instanceof java.util.ArrayList l.add("foo") assertThat l.size(), is(1) def r = l.search("foo") }
在 清單 3 的第一個單元測試中,我通過工廠檢索 Stack,驗證它確實是 Stack,然後執行 Stack的操作,如 push()、size()和 search()。但在第二個單元測試中,我必須用 MissingMethodException 預期發生的異常,以保護測試 ,這樣才能通過測試。在檢索 Array-like 集合並將它作為一個 List 放入變量類型時,我可以證明我確實收到了一個 list。但是,當我試圖調用這個 search()方法時,它會觸發異常,因為 ArrayList不包括 search()方法。因此,通過提供 無編譯時間保護的聲明,可以確保方法調用是正確的。
雖然這看起來可能像一個 bug,但它卻是正確的行為。 Groovy 中的類型只確保 賦值語句的有效性。例如,在 清單 3 中,如果我返回一些 List 接口的內容,則會觸發運行時異 常(GroovyCastException)。這會讓 Groovy 與 Clojure 在強動態類型家族中牢牢站穩腳跟。
然而,語言最近發 生的變化已使 Groovy 中的靜態和動態區分變得十分模糊。Groovy 2.0 新增了一個 @TypeChecked注釋,使您能夠在類或方 法級別上制定嚴格類型檢查特別決策。清單 4 舉例說明了這個注釋:
清單 4. 類型檢查與注釋
@TypeChecked @Test void type_checking() { def f = new CollectionFactory() List l = f.getCollection("Stack-like") l.add("foo") def r = l.pop() assertEquals r, "foo" }
在 清單 4 中,我添加了 @TypeChecked 注釋,驗證了賦值和隨後的方法調用。例如,清單 5 中的代碼不再進 行 編譯:
清單 5. 類型檢查可防止無效的方法調用
@TypeChecked @Test void invalid_type() { def f = new CollectionFactory() Stack s = (Stack) f.getCollection("Stack-like") s.add("foo") def result = s.search("foo") }
在 清單 5 中,我必須為工廠返回添加類型轉換,以支持我調用 Stack的 search()方法。該工具與限制條件一 起使用:在啟用靜態類型時,Groovy 的許多動態特性是無效的。但是,該示例說明了 Groovy 在建立靜態和動態劃分之間 的橋梁時的不斷變化。
所有這些語言都是十分強大的元程序編程工具,所以事後可以添加更加嚴格的類型。例如, 有一些輔助項目可以將選擇類型添加到 Clojure。但是,通常情況下,如果選擇類型是可選的,那麼它就不再是類型系統的 一部分;它是一種驗證機制。
命令式與函數式
另一個主要比照是命令式與函數式。命令式編程主要關注分步 指令,在許多情況下,模仿古老的低級硬件的便利條件。函數式編程更關注一流結構函數,並試圖最大程度地減少狀態轉換 和易變性。
受 Java 影響的比較大的 Groovy 實際上是一種命令式語言。但從一開始它就包含大量函數式語言特性 ,隨著時間的推移,還在不斷添加新的特性。
Scala 將這兩種范式結合起來使用並為它們提供支持。雖然函數式編 程更受喜歡(並受支持),但 Scala 仍然支持面向對象的編程和命令式編程。因此,恰當使用 Scala 要求一個紀律嚴明的 團隊確保您不會將范式隨意混合搭配,這在多范式語言中一直是一種危險舉動。
Clojure 沒有提供糾錯功能。它支 持面向對象的編程,以便支持與其他 JVM 語言輕松交互,但它並不會充當橋梁。相反,Clojure 的專斷設計聲明設計師所 思考的正是良好的工程實踐。那些決策影響深遠,使 Clojure 能夠以開創性的方式解決 Java 世界冗繁的問題(如並發性 )。
了解這些全新語言所需的諸多思維轉換都來自命令式 / 函數式劃分,而且這是本系列文章中最有意義的探索領 域之一。
結束語
開發人員生活在一個采用多種語言解決問題的語言種類越來越多的世界中。學會有效使用新語言有助於您確定某種方法 何時適用。您無需拋棄 Java,它將逐漸融合下一代 JVM 語言的特性;通過了解它們,您可以窺視 Java 語言的未來。
在下一期的 Java 下一代語言中,我將開始對比 Groovy、Scala 和 Clojure 之間的共同之處。