本文是ThoughtWorks公司架構師Neal Ford在IBM developerWorks系列文章java.next中的第一篇,其基於Groovy,Scala和Clojure,講述了多語言編程的重要性,並對靜態類型與動態類型,函數式編程與命令式編程進行了比較。(2013.02.06最後更新)
在我與Martin Fowler曾經合作呈現的一次主題演講中,他作出了一個有洞察性的觀點:
Java的遺產將是平台,而不是程序設計語言。
Java技術的原始工程師們作出了一個明智的決定,就是將編程語言與運行時環境分開,最終這使得超過200種語言能夠運行在Java平台上。這種架構對於該平台的長期活力是至關重要的,因為計算機程序設計語言的壽命一般都是比較短。從2008年開始,由Oracle主辦的一年一度的JVM語言峰會為JVM上其它的語言實現與Java平台工程師進行開放式合作提供了機遇。
歡迎來到Java.next專欄系列,在本系列的文章中,我將講述三種現代JVM語言--Groovy,Scala和Clojure--它提供了范式,設計選擇與舒適因子之間一種有趣的混合。在此我不會花時間去深入介紹每種語言;在它們各自的站點上都有這類深度介紹。但這些語言社區的站點--它們主要目的是為了傳布這些語言--都缺乏客觀的信息,或者是該語言不適用的例子。在本系列的文章中我將進行獨立地比較,以填補上述空白。這些文章將概述Java.next程序設計語言,以及學習它們的好處。
超越Java
Java程序設計語言達到卓越的程度就是,按Bruce Tate在他的Beyond Java一書中的說法,完美風暴:Web應用的興起,已有Web技術由於種種原因不能適應需求,企業級多層應用開發的興起,這些因素共同造就了Java的卓越。Tate也指出這場風暴是一系列獨一無二的事件,曾經沒有其它語言使用相同的途徑達到相同的卓越程序。
Java語言已經證明其在功能方面的強大靈活性,但它的語法與固有范式則存在著長期已知的局限性。盡管一些承諾過的變化即將引入到該語言中,但Java語法卻不能很容易地支持一些重要的未來語法特性,例如函數式編程中的某些特性。但如果你試圖去找到一種語言去替代Java,那麼你就找錯了。
多語言編程
多語言編程--在2006年的一篇博客中我使這個術語重煥活力並重新流行起來--是基於這樣的一種認識:沒有一種編程語言能夠解決每個問題。有些語言擁有某些內建的特性,使其能夠更好地適應特定的問題。例如,由於Swing十分復雜,開發者們發現很難編寫Java中的Swing UI,因為它要求事先聲明類型,為UI動作定義煩人的匿名內部類,還有其它的麻煩事兒。使用更適合構建UI的語言,如Groovy中的SwingBuilder工具,去構建Swing應用會美妙得多。
運行在JVM上的程序設計語言大量增多,這大大激發了多語言編程理念,因為你可以混用編號語言,並可使用最佳匹配的語言,但同時卻維護著相同的底層字節碼和類庫。例如,SwingBuilder並不是要替代Swing;它只是搭建在已有的Swing API之上。當然,在相當長的時間內,開發者們還是將在JVM之外混合使用編程語言--例如,為特定目的而使用SQL和JavaScript--但在JVM的世界內,混合編程將變得更為流行。ThoughtWorks中的許多項目就合用著多種編程語言,而所有由ThoughtWorks Studios開發的工具則都要使用混合語言。
即便Java仍是你主要的開發語言,學習一下其它語言是如何工作的會讓你將它們納入你的未來戰略中。Java仍將是JVM生態系統中的重要組成部分,但最終它更多是作為該平台的匯編語言--或是由於純粹的性能原因,或是在應對特殊需求時才會用到它。
編程語言的進化
當上世紀八十年代我還在大學時,我們使用著一種稱作Pecan Pascal的開發環境。它獨一無二的特性就是能使相同的Pascal代碼既可運行在Apple II上,又可以運行在IBM PC上。Pecan的工程師們為了實現這一目的使用了一種稱作"字節碼"的神秘之物。開發者們將他們的Pascal代碼編譯成"字節碼",該"字節碼"則運行在為各個平台編寫的原生"虛擬機"上。那是一段可怕的經歷!最終程序慢的出奇,即便只是一個簡單的類賦值。當時的硬件無法應對這一挑戰。
Pecan Pascal之後的十年,Sun發布了使用相同架構的Java,它受限也受利於上世紀九十年代的硬件環境。Java還加入了其它的對開發者友好的特性,如自動的垃圾收集。由於曾經使用過像C++之樣的語言,現在我再也不想使用沒有垃圾收集功能的語言去編碼了。我寧願花時間在更高抽象層次上去思考復雜的業務問題,而不是像內存管理這樣的復雜管道問題。
計算機語言通常沒有很長壽命的原因之一就是語言和平台設計的創新速度。由於我們的平台變得更為強大,它們可以處理更多的額外工作。例如,Groovy的內存化(memoization)特性(2010年加入)會緩存函數調用的結果。不需要手工編寫緩存代碼,那樣會引入潛在的缺陷,你僅僅只是需要調用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的下一個版本中將加入高階函數(higher-order function),這使得內存化更容易被實現。通過研究下一代Java語言,你就可以先睹Java的未來特性為快了。
Groovy,Scala和Clojure
Groovy是二十一世紀的Java語法--濃縮咖啡取代了傳統咖啡。Groovy的設計目標是更新並消除Java語法中的障礙,同時還要支持Java語言中的主要編程范式。因此,Groovy要"知曉"諸如JavaBean,它會簡化對屬性的訪問。Groovy會以很快的速度納入新特性,包括函數式編程中的重要特性,這些特性我將在本系列的後續篇章中著重描述。Groovy仍然主要是面向對象的命令式語言。Groovy區別於Java的兩個基本不同點:它是動態而非靜態的;它是的元編程能力要好得多。
Scala從骨子裡就是為了利用JVM而進行設計的,但是它的語法則是完全被重新設計過了。Scala是強靜態類型語言--它的類型要求比Java還嚴格,但造成的麻煩卻很少--它支持面向對象和函數式范式,但更偏好於後者。例如,Scala更喜歡val聲明,這會生成不可變變量(類似於在Java中將變量聲明為final)賦給var,而var將創建更為大家所熟悉的可變變量。通過對這兩種范式的深度支持,Scala為你可能想要的(面向對象的命令式編程)與你所應該想要的(函數式編程)之間架起了一座橋梁。
Clojure是最激進的,它的語法是從其它語言中分離出來,被認為是Lisp的方言。Clojure是強動態類型語言(就像Groovy),它反映了一種義無反顧的設計決策。雖然Clojure允許你與遺留的Java程序進行全面而深度的交互,但是它並不試圖構建一座橋梁去連接面向對象范式。例如,Clojure是函數式編程的鐵桿,也支持面向對象以允許與該種范式進行互操作。盡管它支持面對對象程序員所習慣的全部特性,如多態--但,是以函數式風格,而非面向對象風格進行實現的。設計Clojure時遵循了一組核心的工程原則,如軟件事務內存(Software Transactional Memory),這是為了迎合新功能而打破了舊有的編程范式。
編程范式
除語法之外,這些語言之間的最有趣的不同之處就是類型及其內在的編程范式:函數式或命令式。
靜態類型 vs. 動態類型
編程語言中的靜態類型要求顯式的類型聲明,例如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對象,然後再執行棧操作,例如push(),size()和search()。然而,在第二個單元測試中,我必須聲明一個期望的異常MissingMethodException才能確保該測試能夠通過。當我獲取一個Array-like的集合,並將它賦給List類型的變量時,我能夠驗證返回的類型確為一個List對象。但是,當我試圖調用search()方法時將觸發異常,因為ArrayList並不包含search()方法。因此,這種聲明無法在編譯時確保方法的調用是正確的。
雖然這看起來像是一個缺陷,但這種行為卻是恰當的。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將繼續進行改進,以彌合靜態性與動態性之間的分歧。
所有這些語言都有十分強大的元編程功能,所以更為嚴苛的類型化可以在事後再添加進來。例如,已有多個分支項目將選擇性類型(selective type)引入到Clojure中。但一般認為選擇性類型是可選的,它不是類型系統的一部分;它只是一個類型驗證系統。
查看本欄目
命令式 vs. 函數式
另一個主要的比較維度就是命令式與函數式。命令式編程注重於單步執行的結構,在許多情況下,它是模仿了早期底層硬件的有益結構。函數式編程則注重將函數作為第一等的結構體,以試圖將狀態傳遞與可變性降低到最小。
Groovy在很大程度上是受Java的啟發,它在根本上仍然是命令式語言。但從一開始,Groovy就加入了許多函數式命令的特性,並且以後還會加入更多的此類特性。
Scala則彌合了這兩種編程范式,它同時支持這兩種范式。在更偏向(也更鼓勵)函數式編程的同時,Scala依然支持面向對象和命令式編程。因此,為了恰當地使用Scala,就要求團隊要受到良好的培訓,以確保你不會混用和隨意地選擇編程范式,在多范式編程語言中,這一直都是一個危險。
Clojure是鐵桿的函數式編程語言。它也支持面向對象特性,使得它能夠很容易地與其它JVM語言進行交互,它並不試圖去彌合這兩種范式之間的隔閡。相反,Clojure這種義無反顧的決策使它的設計者所考慮的語句成為很好的工程學實踐。這些決策具有深遠的影響,它使Clojure能夠以開創性的方法去解決Java世界中一些揮之不去的問題(如並發)。
在學習這些新語言時所要求的許多思想上的轉變就是源自於命令式與函數式之間的巨大差別,而這也正是本系列文章所要探索的最有價值的領域之一。
結論
開發者們正生活在一個多語言編程快速發展的世界中,在這種環境中,要求使用多種不同的語言去解決問題。學習高效地利用新語言可以幫助你決定哪種方法是合適的。即便你無法離開Java,它也會逐步地將下一代JVM語言中的特性納入到Java中;現在看看這些新特性,就會使你在潛移默化之中掌握到未來的Java語言。
在本系列的下一篇文章中,我將開始通過探索Groovy,Scala和Clojure中的共通之處來對它們進行比較。