Java靠強大的JVM維護了一個藍領級語言?

下面的文章是冰河寫的,就是一本common lisp書的作者,前網易的一個員工,文章後面的一個評論裏說“java是藍領級語言”,這麼說我們都是民工了,不過稍後仔細想了想,確實是民工,讓我對java有了新的認識!

從 Lisp 程序員的視角看 Java 語言的缺點

Java 語言的優缺點——Lisp 程序員的視角 - 冰河 - Chun Tian (binghe)

引子

帶着三分無奈和七分不情願,終於把 Java 複習了一遍。教材用的是我大學時買的《Java 2 編程指南: SDK 1.4》,雖說老了一些,但書絕對是好書,講得很透徹。我終於想起來了,Java 語言如果是 1995 年左右誕生的話, 我當時在雜誌上讀到了,《大衆軟件》或者《計算機應用文摘》,主標題好像是《Java 來了》。可惜當時我還在學 Turbo C 呢,忙不過來,於是把 Java 忽略了。
那麼 Java 跟 Lisp 之間又有什麼關係呢?首先,Sun 公司 Java 語言規範的制定者之一 Guy Steele 同時也是 Common Lisp 標準化委員會的成員之一,Common Lisp 標準草案文檔《CLTL2》的作者,以及 Lisp 史論文《The Evolution of Lisp》的作者之一,這就意味着 Java 語言在定義的時候深受 Common Lisp 的影響,至少在定義 Java 的時候知道 Lisp 究竟是什麼樣子的;其次,Java 語言發明時引入的一些新特性(虛擬機,GC,流)根本就是來自 Common Lisp 的。
我對 Java 語言的總體理解是,設計者試圖實現一個 OO 語言,它要在語法上儘可能接近 C,運行時環境上接近 Lisp,OO 部分則需要解決 C++ 中的一些難題。最後得到的是一個醜陋的設計,而且經常拆東牆補西牆。Java 的唯一創新應該是強制的軟件包(庫)管理系統,這對實現軟件工程卻極其有利。鋪天蓋地的 jar 包極大地擴展了 Java 語言的應用範圍,組件重用也變得輕而易舉了。最後,各種 Java IDE 彌補了程序中廢話太多的不足。

非 OO 部分

Java 雖然有 GC 系統幫忙清理內存,但整個語言似乎在鼓勵程序員肆意浪費內存,我從 hello world 上就看到這點了。爲了生成格式化的輸出,Java 提供了 System.out.println(),其地位相當於 C 的 printf() 和 Common Lisp 的 format。Java 版本是最浪費內存的,因爲它在運行期是通過字符串拼接的方式來產生需要輸出的最終字符串的,而字符串拼接操作的所有中間結果以及最終結果在輸出完成以後都要被丟棄,然後等待 GC。相比之下,printf 或 format 的格式化字符串更像是一段執行輸出操作的微程序,不但表達能力上來了,格式字符串本身也不存在運行期的自我複製。
Java 數據的創建過程和 C 差不多,允許對數據進行靜態初始化。問題是數組初始化語法 { … } 不但侷限性很大(無法簡單地將所有數組元素初始化成同一個值),而且該語法本身並不是一個合法的表達式,但卻可以寫在等號的後面,從而給編譯器帶來了額外的負擔。相比之下,Common Lisp 的數組是由一個普通的函數 make-array 生成的,不但接受用來初始化數組元素的列表,還接受用來初始化整個數組的單個值;更重要的是,通過使用特殊的關鍵字參數,Common Lisp 的數組是可變大小的,必要時還存在類似指針的配套遊標對象 (fill-pointer) 以支持靈活地向數組中輸入數據。
Java 把所有從 C 那裏過繼來的基本數據類型又給重新封裝了一次,例如 int 封裝成了 java.lang.Integer。這樣做真的有必要嗎?我看也未必。究其根源,Java 語言雖然讓類 (class) 成爲程序的最基本元素了,卻沒有配套地把所有的函數 (function) 都變成方法 (method)。諸如 sin/cos 和 max/min 這樣的操作符仍然沿用了 C 語法,但 Java 設計者卻不能接受更多的這類全局函數了,於是創造了基本數據類型的封裝類,然後把更多的高級運算符以類方法的形式只供封裝類的對象使用。Common Lisp 也有對象系統,稱爲 CLOS。知道 CLOS 是怎麼做的嗎?所有的方法調用 (method call) 都跟普通函數調用在形式上是一樣的,而所有基本數據類型直接被併入 CLOS 的類層次體系了,在 Common Lisp 中,如果單純觀察一段用戶代碼的話,甚至無法鑑別究竟一個操作符是函數還是方法。我們把具有相同名稱的所有方法稱爲廣義函數 (generic function)。
P. S. 近年來某些更噁心的語言——我不確定是 Python 還是 Ruby——試圖避免 Java 的這種尷尬,直接允許基本數據類型作爲對象使用,例如 sin(1) 可以寫成 1.sin()。這在一方面說明 Java 在這個地方確實設計得不怎麼樣,另一方面即便這麼做也是誤入歧途了。一門語言中所有不同類型的子程序調用都應該具有統一的形式,無論是普通函數還是具有多態性的方法 (method),這纔是最美的設計。你們寫 1+1 時,我們寫 (+ 1 1);你們寫sin(x) 時,我們寫 (sin x)你們說 you.fuck() 時,我們可以說 (fuck you) !!!
Java 的字符串系列操作符(String, StringBuffer, StringTokenizer, interning, …)大概是整個基礎語言中花費心思最多的部分了。這部分的主要問題是 “正交性“ 不足。就是說,字符串這種數據類型事實上包含了兩個屬性,首先它是一個串,也就是向量或者一維數組,其次它是由字符所組成的。一個充分正交的語言應當把串操作符和字符操作符分開定義,並讓前者可在向量或一維數組上使用。比如說 Java 定義了一些在字符串中做查找和替換之類的方法,但這些事情其實在一維數組裏也是有用的;而另一個方法,比如說檢測整個字符串是否全部由數字或字母所構成,或者在不考慮大小寫的前提下比較兩個字符串的內容,這些纔是 String 類的份內工作!Common Lisp 的基本數據類型是具有層次關係的,一維數組 (也稱爲向量) 和列表通稱爲序列 (sequence),並且諸如查找、替換和著名的 map 與 reduce 函數都是用於一般性序列的操作符。C++ 的 STL 也有類似的特徵,不知道是不是跟 Lisp 學的。
P. S. Java 的字符數組和字符串是不同的類型?一切都是字符串整體作爲一個對象所惹的禍。

OO 部分

Java 語言的 OO 部分整體感覺比 C++ 略強一些,但很多 C++ 的 OO 問題並不是真的解決了,而是被語言直接禁止了。(比較遺憾的是我 Objective-C 不熟,沒法比較,這麼多年蘋果電腦算是白用了)
Java 類名和程序中的變量名似乎是在同一個名字空間的。這是因爲 Java 在調用類的靜態方法或靜態成員時時是將類的名字方法對象的位置上,例如 System.out 以及 Class.forName()。這恐怕就是爲什麼 Java 教材中建議所有變量的名字都採用小寫開頭,而所有類的名字都用大寫開頭的緣故,怕程序員一不小心就名字衝突了。我相信 Java 編譯器纔不管這一套,所有出現在 . 之前的符號在編譯期都要仔細地檢查它究竟是附近定義的一個變量,還是來自遙遠 jar 包的一個類名。Common Lisp 怎麼處理靜態成員的問題?我們可以用 MOP 的 class-prototype 函數從任何類中提取出一個原型對象來,然後就像使用正規對象一樣來使用它。而且由於類的實例化過程是通過普通函數實現的,類的名字有自己的命名空間,跟函數、變量同名也沒有關係。
嵌套類的存在就是一個悲劇,還嫌不夠亂嗎?我們接受局部函數是因爲這可以消除重複的模式,讓局部代碼可重用;我們接受局部變量是因爲這些東西可以幫助我們緩存中間結果;嵌套類有什麼意義?類是對象結構的描述,這點兒破事兒難道還要掖着藏着不讓整個程序知道嗎?Java 書的這個地方我沒仔細看,但如果一個嵌套類的實例被傳給了完全無關的其他類的話,嵌套類的私有方法還能隨便地被調用嗎?
P. S. 我可以接受匿名類及其存在的理由,但 Java 編譯器不應該針對每個匿名類 (還有嵌套類) 都分別編譯出單獨的 .class 文件啊!ABCL 源代碼中的一個 .java 文件經常可以被編譯出超過 100 個 .class 文件,這不是精神病嘛。
Java 對多繼承問題的妥協。我聽說 C++ 裏麻煩的鑽石繼承問題,推薦的解決方案是改用虛繼承;Java 用一種不允許帶有成員變量的特殊類——接口 (interface),把這個事情給避開了。爲什麼類不能多繼承而接口就可以呢?哦,因爲 Java 類的繼承過程是跟 C++ 學的,子類的數據結構直接掛接在基類數據結構的後面,子類所定義的成員變量都被認爲是全新的,而無論其名字是否與某個基類的成員同名。多繼承是必需的,因爲整個世界在本體論的意義上確實是單根多繼承的。於是接口作爲一種半殘廢的類出現了——它只允許有象徵性的成員函數,而決不允許擁有成員變量。這樣接口多繼承中的鑽石繼承問題總算是混過去了,但這樣搞出來的一切都是虛的,爲了讓這些接口類能真正的用來做事,你不得不用一個類來配合它,給它注入成員變量和實際的方法代碼。
Common Lisp 對象系統 (CLOS) 是如何處理鑽石繼承問題的?簡單地說,我們沒有必要處理。因爲所有類層次關係中同名的成員變量都被認爲是同一個!但是子類爲什麼要重複地定義基類已有的成員變量呢?因爲它需要特化基類的成員類型和其他屬性,例如基類的某個成員是數值類型的,那麼子類可以進一步說它是整型的,這是有意義的。Common Lisp 之所以能做到這點,是因爲 Lisp 系統有權限訪問所有那些基類的成員清單,但 Java 和 C++ 似乎都不可以。當然,如果允許同名的成員變量被視爲等價的話,名字空間的問題就再次浮出水面了。Java 似乎把 C++ 的 namespace 特性直接幹掉了,這樣一來,如果採用了 Common Lisp 的解決方案,那麼名字衝突就太可怕了,隨便給私有成員變量起個名字就可能跟某個上層基類的同名成員相沖突,這顯然是不好的。

後記

敝人的 Java 純屬初學,以上關於 Java 特性的描述如有失當之處,希望有關讀者予以指出,深表謝意。同時歡迎廣大 Java 程序員們在本文的評論中發表自己的有關觀點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章