萬能 Java

青牛發表於2015-03-19

我常常問面試者,“你最喜歡的程式語言是什麼?” 答案几乎如出一轍,“工作中我只選擇正確的程式語言。” 廢話,誰會故意選擇錯誤的語言呢?這顯然是為了逃避選擇一種具體的程式語言,以免選擇了一種我不喜歡的。

如果面試者這樣回答“我最熟悉某一種程式語言”,這同樣也沒有回答我的問題。

當時要是我的話,我會這樣回答,“我最喜歡 Python,因為使用它程式設計讓我感到快樂,但我只在某某情況下使用它。其餘時間,我使用 XYZ...”

然而,大約一年之前,我產生了一個奇怪的想法:Java 適合所有的程式設計工作。(在你吐槽之前,我暫停一下,)這個想法根植於你感覺正確但卻與現實不符的一些觀點,而且這個想法從來都沒有流行起來,但不管怎樣,請讓我先來解釋一下。

Python 的確是我喜愛的程式語言,用它程式設計真的讓我感到快樂。它讓我的大腦感到快樂,它和虛擬碼是如此契合,以至於用它來工作能讓人真正感到愉悅。

多年以前,我曾經讀過 Bruce Eckel 一本頗具影響力的書《強型別與強測試》(Strong Type vs. Strong Testing)。在書中,他聲稱靜態型別(他稱為強型別)是保證程式正確性的多種方式之一,如果你用單元測試去檢查其它方面(例如演算法和邏輯),那麼型別也將得到檢查,因此你不妨採用動態型別程式語言,並從中獲得動態型別程式語言的優勢。

Bruce 使用了 Python 來說明他的程式碼,其成效非常顯著:我決定從今往後寫什麼都用 Python。不幸的是,工作中一個大型 Java 專案進展到中途時,我和同事一致認為這個程式應該用 Python 來寫,也許有一天,我們會找到一個很好的藉口來重寫這個程式。

不到一年時間,幾件事情讓我的想法來了一個180度的大轉彎:

  • 在一家公司裡,我寫了一個模擬器,這樣就可以讓我的 Java 服務獨立執行而無需一個全功能的網站。在這個模擬器中,我執行一些指令碼測試包括失敗在內不同的情景。由於 Java 6 已經內建了 JavaScript 的解析引擎以及很多人都瞭解這門指令碼語言的緣故,我決定使用 JavaScript 來編寫這些指令碼。我認為使用指令碼語言可以讓我們和測試人員很容易地編寫測試。一位名叫 Justin Lebar 的實習生認為我們應該只使用 Java。模擬器是用 Java 寫的,測試指令碼為什麼不用 Java 呢?它已經在那裡了,這是大家都知道的。我們仍舊堅持使用 JavaScript。在整個過程中,我不得不寫各種各樣的的程式碼讓 Java 和 JavaScript 相互溝通。你知道,因為無法定位到具體指令碼執行的所在行數,這意味著不同語言堆疊的足跡已經變得難以跟蹤了。測試人員無法完成任何測試。到最後,我們從 JavaScript 中一無所獲,Justin 的觀點無疑是正確的。
  • 還是在那家公司裡,我們使用 JSON 格式(順便說一下,這是個很好的想法)來儲存我們的日誌檔案,一位同事寫了一個名為 logcat 的 Python 程式,用來解析日誌檔案和輸出標準的柱狀圖報告,這個程式有許多不錯的功能特性(包括一個二分查詢時間戳)。在我的一個私人專案中,也需要類似的東西,我再次建議使用 Python。但我的搭檔 Dan Collens 則認為應該使用 Java,因為它已經在那裡了,我們都瞭解它,而且它夠快。他最終用 Java 實現了它,我的搭檔是對的:它的速度極快。我對 Python logcat 和 logcat 的另一個 Java 實現作了一番對比,後者的速度大約快十倍。 使用 Python 去實現,無論程式設計師節省了多少時間都無法挽回失去使用者的損失,因為很多使用者每次不得不等上十倍的時間才可以拿到分析日誌報告。
  • 最後一個例子,我編寫了一個簡單的程式用於搭建一個 Web 介面。我覺得應該使用 Python,但是這樣做的話,我需要找出如何利用 Python 的類庫來為 Web 頁面提供服務的辦法。我在此之前已經在 Java(採用 Jetty)中實現過這個功能了,所以使用 Java 的話,我可以在更短的時間內將其做好並且執行起來。這個時候,我開始意識到,隨著我在第三方 Java 庫上面的知識積累以及在實用工具方面的不斷成長,使用其它語言的成本已經變得越來越高了。我需要把這些事情搞清楚再寫一遍,而不是從已有的專案中複製和貼上。注意,這不僅適用於 Java,它也適用於任何使用單一語言的場景。

對於 Java,最大的爭論在於它的語法繁瑣。可能吧,但那又怎樣呢?我認為真正的爭論是寫 Java 程式碼需要更長的時間。我懷疑在最初的10分鐘之後這是很真實的。當然你不得不寫 public static void main,但那又能花多少時間呢?當然你也必須這樣寫:

Map<String,User> userIdMap = new HashMap<String,User>();

而不是:

userIdMap = {}

如果從一個更大的情景來看,這算長嗎?一天之中能多花多少分鐘,2天呢?而在 Python 更加接近實際的程式碼看起來是這樣:

# Map from user ID to User object.
userIdMap = {}

(如果沒按照上述方式編寫 Python 程式碼,那麼你就可能面臨更大的問題。缺乏文件的 Python 程式是很難維護的。)我們面臨的問題是,程式設計師認為不用動腦筋的工作是痛苦且浪費時間。我在這裡引用某個論壇中的一條帖子,很好地證明了這一點:

當你不得不為極其明顯的物件增加型別宣告時,沒有比這更無聊的了,比如 Foo x = new Foo()。– @pazsxn

實際上不是這樣,額外來鍵入 Foo 並非“很無聊”。只是三個字元而已。由於這項工作無需動腦,致使其副作用被嚴重誇大了,但這只是小事一樁。程式設計師真正不願意做的事,是為了處理列表事項而編寫某種型別的命令:

if command = "up":
     up()
elif command = "status":
     status()
elif command = "revert":
     revert()
...

因此,他們將會編寫一些自認為聰明的自動排程程式碼,但這樣做需要花費更長的時間,而且肯定會讓後來的程式設計師產生迷惑:revert() 方法是怎樣呼叫的呢。即便如此,程式設計師卻錯誤地覺得好像這樣做會是在節約時間。這其實是一個動態語言的陷阱。它讓你自我感覺更有效率,但除了編寫一個新程式的前10分鐘之外,其他時間並非如此。你只是通過手工編寫了一些愚蠢的排程程式碼,到最後,你還得在那些真正的工作上花費精力。

(題外話:關於使用 vim 編輯器編寫程式碼這一問題,我對不同選擇的理解有誤。我感覺使用 vim 非常有效率,似乎程式碼在終端上飛舞,而使用 Eclipse 讓我感覺遲鈍,這證明了我的選擇更傾向於效率。但顯然我的收益是建立在一定損失的基礎之上的,我不得不檢視誰呼叫了一個特定函式,或者不得不手動檢視一個物件的方法。動態語言的支持者們有他們自己的選擇,我承認在這一問題上我同樣是錯的。)

那麼到底為什麼要選擇動態語言呢?如果你和我比賽去寫一個簡單的部落格系統,你用 Python 的話,使用 pickling 和 whatnot 你在30分鐘內就會讓事情變得有趣,而我用 MySQL 構建的話要花2天時間。許多語言方面的選擇是基於類似這樣的微不足道的競賽。但在大約兩週開發之後,當我們都需要增加一個功能時,我花的時間最多和你一樣,而且我不需要在如何讓我的系統應對大量使用者上花費任何時間,或者追蹤那些令人困惑的無效語句,其原因只是你的一個函式名拼寫錯誤導致語句執行中斷,或者想弄明白這個請求引數到底包含什麼東西,悲催呀。

黑客級程式設計師蔑視“規範和要求嚴格的語言”是目光短淺的表現。大型、長期使用且由多名程式設計師參與的專案和自用的快速原型專案是完全不同的東西。– Source

而且,你覺得你可能不會很快碰到程式的可伸縮性問題?NaNoWriMo 網站在每年的10月31號都會當機,停止響應好多天。在這一期間的幾個小時內,大約有60,000使用者訪問,每秒可能有4個請求。這個網站是用 PHP 寫的。OurGroceries 的後端是用 Java 寫的,它能處理(當前)大約每秒50個複雜請求,而對於 Java 執行緒來說 CPU 很少超過1%。

Twitter 最近通過把搜尋引擎從 Ruby 切換到 Java,將查詢速度提升了三倍。

一年之前,Joel Spolsky 發表推文:

Digg: 200MM 頁面瀏覽,500臺伺服器。Stack Overflow: 60MM 頁面瀏覽,5臺伺服器。我漏掉什麼了嗎?

— Joel Spolsky (@spolsky) October 13, 2010

@GregB 的反饋是:

@spolsky: Digg: 200MM 頁面瀏覽,500臺伺服器。Stack Overflow: 60MM 頁面瀏覽,5臺伺服器。我漏掉什麼了嗎?<< 這就是 PHP 的原因。

— Greg Brant (@GregB) October 13, 2010

StackOverflow 使用的是 ASP.NET。因此你會整天抱怨 public static void main,但需要搭建500臺伺服器這一事實也太可笑了。動態語言的缺點是真實存在的,代價也很昂貴,而且會一直延續下去。

那麼單元測試這個觀點又怎麼樣呢?如果你必須對你的程式碼進行單元測試,靜態型別能為你帶來什麼呢?好吧,它能加快速度,而且是大幅度的。但是編寫和維護單元測試也需要耗費時間。最重要的是,最常出現的 bug 並非單元測試都能完全覆蓋。除了少數例外情況(例如解析器),單元測試是在浪費時間。引用我哥們的一段話,“單元測試是一種冗長且易於出錯的方法,它試圖挽回由於缺乏靜態型別註解而失去的價值,但卻以一種笨手笨腳的形式出現,因為它和實際業務程式碼本身是完全分離的。”

因此,我的新思路就是:做任何事都用 Java。不要試圖使用 Python 寫一些可以快速實現的黑客程式碼,因為:

  • 你無法從使用主要程式語言開發的專案中複製和黏貼程式碼。//實現程式碼重用
  • 開發起來可能感覺會快一些,但這是假象。實際節省的時間非常有限,儘管有些語法特徵的確讓人討厭。
  • 我和我的同事不得不學習和掌握另一門語言、平臺以及一系列類庫。
  • 還有一個重要原因:很可能,這個快速實現的黑客程式碼將會成長為一個重要的工具,我沒有時間去重寫它,因而每次使用它我都要忍受由於效能不佳和難於維護而導致的懲罰。

使用 Python 開發是快樂的,我同意這個觀點。我熱愛 Python。當我寫一個數獨求解程式時,我會使用 Python。但是對於更大些的專案,它是個錯誤的工具,而且對於和支付有關的任何規模的專案來說,它也是個錯誤的工具,因為這可能會給你的老闆帶來一定損失。

我甚至想把這個思路發揮到極致 - 使用 Java 編寫 shell 指令碼。除了一個簡單的包裝器之外,我發現 shell 指令碼最終都會發展到一種情景,即僅僅為了從 bash 中的一個陣列中移除一些中間元素,需要我在晦澀難懂的語法中反覆尋找方法。這是多麼蹩腳的語言啊!錯誤的工具呀!還是使用 Java 吧。如果你覺得在 shell 上執行命令顯得很愚蠢,編寫一個工具函式就可以解決這個問題。

我已經編寫了一個 Java 啟動器指令碼,這樣我就可以將其寫在 Java 程式的頭部:

#!/usr/bin/env java_launcher
# vim:ft=java
# lib:/home/lk/lib/teamten.jar

我可以讓這個 Java 程式執行,並且同時去掉 .java 副檔名。指令碼提取頭部內容,編譯並快取 class 檔案,然後使用指定的 jar 包去執行。這原本是 Python 的特有優勢:對於簡單的一次性程式,就無需構建指令碼啦。

專注於單一的語言將會產生一個有趣的影響:激勵我完善我的個人工具函式庫(上面的 teamten.jar),因為我的努力不再需要分散到幾個語言之中了。舉個例子,我寫了一個圖片處理的類庫。和你在 Java 和 Python 中能找到的任何類庫相比,這個類庫不僅速度快而且質量更高。這花費了我的一些時間,但我認為這是值得的,因為我發現,我無法使用 Python 指令碼更好地更好地實現裁剪一張圖片的功能。我現在可以充滿自信地把對 Java 的投資作為我未來職業和個人技術的一個重要組成部分。

最後還有一個在眾多編譯型靜態型別語言中,我為什麼特別選擇 Java 的問題。C 和 C++ 的優勢(輕微的效能優勢,可嵌入性,適合編寫圖形化庫)不適用於我的工作。C# 挺不錯,但不是跨平臺的。Scala 太複雜了。其他語言像 D 和 Go 都太新了,因此我不能把工作賭在它們上面。

每當我告訴人們我現在寫什麼都用 Java 時,他們看起來都很恐懼的樣子。甚至有一位朋友明顯面帶厭惡的表情。但是你知道嗎,Java 是一門相當好的語言,當我進行程式碼編譯時,往往在第一時間,它通常會正確地執行。任何其它語言都沒有像 Java 那樣給予我心靈上的寧靜。Java 就像一匹兢兢業業的寶馬良駒,對於各種型別的應用程式來說,它都適用。


作者:Lawrence Kesteloot,是一名自由程式設計師,住在舊金山,喜歡林迪舞、法語、旅行、以及結對程式設計。

原文: Java for Everything

感謝: Jodoo 幫助審閱並完成校對。

P.S. 如果您喜歡這篇文章並且希望學習程式設計技術的話,請關注一下 復唧唧

相關文章