引子

  帶著三分無(wú)奈和七分不情愿,終于把 Java 復(fù)習(xí)了一遍。教材用的是我大學(xué)時(shí)買的《Java 2 編程指南: SDK 1.4》,雖說(shuō)老了一些,但書絕對(duì)是好書,講得很透徹。我終于想起來(lái)了,Java 語(yǔ)言如果是 1995 年左右誕生的話, 我當(dāng)時(shí)在雜志上讀到了,《大眾軟件》或者《計(jì)算機(jī)應(yīng)用文摘》,主標(biāo)題好像是《Java 來(lái)了》。可惜當(dāng)時(shí)我還在學(xué) Turbo C 呢,忙不過(guò)來(lái),于是把 Java 忽略了。

  那么 Java 跟 Lisp 之間又有什么關(guān)系呢?首先,Sun 公司 Java 語(yǔ)言規(guī)范的制定者之一 Guy Steele 同時(shí)也是 Common Lisp 標(biāo)準(zhǔn)化委員會(huì)的成員之一,Common Lisp 標(biāo)準(zhǔn)草案文檔《CLTL2》的作者,以及 Lisp 論文《The Evolution of Lisp》的作者之一,這就意味著 Java 語(yǔ)言在定義的時(shí)候深受 Common Lisp 的影響,至少在定義 Java 的時(shí)候知道 Lisp 究竟是什么樣子的;其次,Java 語(yǔ)言發(fā)明時(shí)引入的一些新特性(虛擬機(jī),GC,流)根本就是來(lái)自 Common Lisp 的。

  我對(duì) Java 語(yǔ)言的總體理解是,設(shè)計(jì)者試圖實(shí)現(xiàn)一個(gè) OO 語(yǔ)言,它要在語(yǔ)法上盡可能接近 C,運(yùn)行時(shí)環(huán)境上接近 Lisp,OO 部分則需要解決 C++ 中的一些難題。最后得到的是一個(gè)丑陋的設(shè)計(jì),而且經(jīng)常拆東墻補(bǔ)西墻。Java 的唯一創(chuàng)新應(yīng)該是強(qiáng)制的軟件包(庫(kù))管理系統(tǒng),這對(duì)實(shí)現(xiàn)軟件工程卻極其有利。鋪天蓋地的 jar 包極大地?cái)U(kuò)展了 Java 語(yǔ)言的應(yīng)用范圍,組件重用也變得輕而易舉了。最后,各種 Java IDE 彌補(bǔ)了程序中廢話太多的不足。

  非 OO 部分

  Java 雖然有 GC 系統(tǒng)幫忙清理內(nèi)存,但整個(gè)語(yǔ)言似乎在鼓勵(lì)程序員肆意浪費(fèi)內(nèi)存,我從 hello world 上就看到這點(diǎn)了。為了生成格式化的輸出,Java 提供了 System.out.println(),其地位相當(dāng)于 C 的 printf() 和 Common Lisp 的 format。Java 版本是最浪費(fèi)內(nèi)存的,因?yàn)樗谶\(yùn)行期是通過(guò)字符串拼接的方式來(lái)產(chǎn)生需要輸出的最終字符串的,而字符串拼接操作的所有中間結(jié)果以及最終結(jié)果在輸出完成以后都要被丟棄,然后等待 GC。相比之下,printf 或 format 的格式化字符串更像是一段執(zhí)行輸出操作的微程序,不但表達(dá)能力上來(lái)了,格式字符串本身也不存在運(yùn)行期的自我復(fù)制。

  Java 數(shù)據(jù)的創(chuàng)建過(guò)程和 C 差不多,允許對(duì)數(shù)據(jù)進(jìn)行靜態(tài)初始化。問(wèn)題是數(shù)組初始化語(yǔ)法 { ... } 不但局限性很大(無(wú)法簡(jiǎn)單地將所有數(shù)組元素初始化成同一個(gè)值),而且該語(yǔ)法本身并不是一個(gè)合法的表達(dá)式,但卻可以寫在等號(hào)的后面,從而給編譯器帶來(lái)了額外的負(fù)擔(dān)。相比之下,Common Lisp 的數(shù)組是由一個(gè)普通的函數(shù) make-array 生成的,不但接受用來(lái)初始化數(shù)組元素的列表,還接受用來(lái)初始化整個(gè)數(shù)組的單個(gè)值;更重要的是,通過(guò)使用特殊的關(guān)鍵字參數(shù),Common Lisp 的數(shù)組是可變大小的,必要時(shí)還存在類似指針的配套游標(biāo)對(duì)象 (fill-pointer) 以支持靈活地向數(shù)組中輸入數(shù)據(jù)。

  Java 把所有從 C 那里過(guò)繼來(lái)的基本數(shù)據(jù)類型又給重新封裝了一次,例如 int 封裝成了 java.lang.Integer。這樣做真的有必要嗎?我看也未必。究其根源,Java 語(yǔ)言雖然讓類 (class) 成為程序的最基本元素了,卻沒(méi)有配套地把所有的函數(shù) (function) 都變成方法 (method)。諸如 sin/cos 和 max/min 這樣的操作符仍然沿用了 C 語(yǔ)法,但 Java 設(shè)計(jì)者卻不能接受更多的這類全局函數(shù)了,于是創(chuàng)造了基本數(shù)據(jù)類型的封裝類,然后把更多的高級(jí)運(yùn)算符以類方法的形式只供封裝類的對(duì)象使用。Common Lisp 也有對(duì)象系統(tǒng),稱為 CLOS。知道 CLOS 是怎么做的嗎?所有的方法調(diào)用 (method call) 都跟普通函數(shù)調(diào)用在形式上是一樣的,而所有基本數(shù)據(jù)類型直接被并入 CLOS 的類層次體系了,在 Common Lisp 中,如果單純觀察一段用戶代碼的話,甚至無(wú)法鑒別究竟一個(gè)操作符是函數(shù)還是方法。我們把具有相同名稱的所有方法稱為廣義函數(shù) (generic function)。

  P. S. 近年來(lái)某些更惡心的語(yǔ)言——我不確定是 Python 還是 Ruby——試圖避免 Java 的這種尷尬,直接允許基本數(shù)據(jù)類型作為對(duì)象使用,例如 sin(1) 可以寫成 1.sin()。這在一方面說(shuō)明 Java 在這個(gè)地方確實(shí)設(shè)計(jì)得不怎么樣,另一方面即便這么做也是誤入歧途了。一門語(yǔ)言中所有不同類型的子程序調(diào)用都應(yīng)該具有統(tǒng)一的形式,無(wú)論是普通函數(shù)還是具有多態(tài)性的方法 (method),這才是最美的設(shè)計(jì)。你們寫 1+1 時(shí),我們寫 (+ 1 1);你們寫 sin(x) 時(shí),我們寫 (sin x);你們說(shuō) you.fuck() 時(shí),我們可以說(shuō) (fuck you) !!!

  Java 的字符串系列操作符(String, StringBuffer, StringTokenizer, interning, ...)大概是整個(gè)基礎(chǔ)語(yǔ)言中花費(fèi)心思最多的部分了。這部分的主要問(wèn)題是 "正交性“ 不足。就是說(shuō),字符串這種數(shù)據(jù)類型事實(shí)上包含了兩個(gè)屬性,首先它是一個(gè)串,也就是向量或者一維數(shù)組,其次它是由字符所組成的。一個(gè)充分正交的語(yǔ)言應(yīng)當(dāng)把串操作符和字符操作符分開定義,并讓前者可在向量或一維數(shù)組上使用。比如說(shuō) Java 定義了一些在字符串中做查找和替換之類的方法,但這些事情其實(shí)在一維數(shù)組里也是有用的;而另一個(gè)方法,比如說(shuō)檢測(cè)整個(gè)字符串是否全部由數(shù)字或字母所構(gòu)成,或者在不考慮大小寫的前提下比較兩個(gè)字符串的內(nèi)容,這些才是 String 類的份內(nèi)工作!Common Lisp 的基本數(shù)據(jù)類型是具有層次關(guān)系的,一維數(shù)組 (也稱為向量) 和列表通稱為序列 (sequence),并且諸如查找、替換和著名的 map 與 reduce 函數(shù)都是用于一般性序列的操作符。C++ 的 STL 也有類似的特征,不知道是不是跟 Lisp 學(xué)的。

  P. S. Java 的字符數(shù)組和字符串是不同的類型?一切都是字符串整體作為一個(gè)對(duì)象所惹的禍。

  OO 部分

  Java 語(yǔ)言的 OO 部分整體感覺(jué)比 C++ 略強(qiáng)一些,但很多 C++ 的 OO 問(wèn)題并不是真的解決了,而是被語(yǔ)言直接禁止了。(比較遺憾的是我 Objective-C 不熟,沒(méi)法比較,這么多年蘋果電腦算是白用了)

  Java 類名和程序中的變量名似乎是在同一個(gè)名字空間的。這是因?yàn)?Java 在調(diào)用類的靜態(tài)方法或靜態(tài)成員時(shí)是將類的名字放在對(duì)象的位置上,例如 System.out 以及 Class.forName()。這恐怕就是為什么 Java 教材中建議所有變量的名字都采用小寫開頭,而所有類的名字都用大寫開頭的緣故,怕程序員一不小心就名字沖突了。我相信 Java 編譯器才不管這一套,所有出現(xiàn)在 . 之前的符號(hào)在編譯期都要仔細(xì)地檢查它究竟是附近定義的一個(gè)變量,還是來(lái)自遙遠(yuǎn) jar 包的一個(gè)類名。Common Lisp 怎么處理靜態(tài)成員的問(wèn)題?我們可以用 MOP 的 class-prototype 函數(shù)從任何類中提取出一個(gè)原型對(duì)象來(lái),然后就像使用正規(guī)對(duì)象一樣來(lái)使用它。而且由于類的實(shí)例化過(guò)程是通過(guò)普通函數(shù)實(shí)現(xiàn)的,類的名字有自己的命名空間,跟函數(shù)、變量同名也沒(méi)有關(guān)系。

  嵌套類的存在就是一個(gè)悲劇,還嫌不夠亂嗎?我們接受局部函數(shù)是因?yàn)檫@可以消除重復(fù)的模式,讓局部代碼可重用;我們接受局部變量是因?yàn)檫@些東西可以幫助我們緩存中間結(jié)果;嵌套類有什么意義?類是對(duì)象結(jié)構(gòu)的描述,這點(diǎn)兒破事兒難道還要掖著藏著不讓整個(gè)程序知道嗎?Java 書的這個(gè)地方我沒(méi)仔細(xì)看,但如果一個(gè)嵌套類的實(shí)例被傳給了完全無(wú)關(guān)的其他類的話,嵌套類的私有方法還能隨便地被調(diào)用嗎?

  P. S. 我可以接受匿名類及其存在的理由,但 Java 編譯器不應(yīng)該針對(duì)每個(gè)匿名類 (還有嵌套類) 都分別編譯出單獨(dú)的 .class 文件啊!ABCL 源代碼中的一個(gè) .java 文件經(jīng)常可以被編譯出超過(guò) 100 個(gè) .class 文件,這不是精神病嘛。

  Java 對(duì)多繼承問(wèn)題的妥協(xié)。我聽說(shuō) C++ 里麻煩的鉆石繼承問(wèn)題,推薦的解決方案是改用虛繼承;Java 用一種不允許帶有成員變量的特殊類——接口 (interface),把這個(gè)事情給避開了。為什么類不能多繼承而接口就可以呢?哦,因?yàn)?Java 類的繼承過(guò)程是跟 C++ 學(xué)的,子類的數(shù)據(jù)結(jié)構(gòu)直接掛接在基類數(shù)據(jù)結(jié)構(gòu)的后面,子類所定義的成員變量都被認(rèn)為是全新的,而無(wú)論其名字是否與某個(gè)基類的成員同名。多繼承是必需的,因?yàn)檎麄€(gè)世界在本體論的意義上確實(shí)是單根多繼承的。于是接口作為一種半殘廢的類出現(xiàn)了——它只允許有象征性的成員函數(shù),而決不允許擁有成員變量。這樣接口多繼承中的鉆石繼承問(wèn)題總算是混過(guò)去了,但這樣搞出來(lái)的一切都是虛的,為了讓這些接口類能真正的用來(lái)做事,你不得不用一個(gè)類來(lái)配合它,給它注入成員變量和實(shí)際的方法代碼。

  Common Lisp 對(duì)象系統(tǒng) (CLOS) 是如何處理鉆石繼承問(wèn)題的?簡(jiǎn)單地說(shuō),我們沒(méi)有必要處理。因?yàn)樗蓄悓哟侮P(guān)系中同名的成員變量都被認(rèn)為是同一個(gè)!但是子類為什么要重復(fù)地定義基類已有的成員變量呢?因?yàn)樗枰鼗惖某蓡T類型和其他屬性,例如基類的某個(gè)成員是數(shù)值類型的,那么子類可以進(jìn)一步說(shuō)它是整型的,這是有意義的。Common Lisp 之所以能做到這點(diǎn),是因?yàn)?Lisp 系統(tǒng)有權(quán)限訪問(wèn)所有那些基類的成員清單,但 Java 和 C++ 似乎都不可以。當(dāng)然,如果允許同名的成員變量被視為等價(jià)的話,名字空間的問(wèn)題就再次浮出水面了。Java 似乎把 C++ 的 namespace 特性直接干掉了,這樣一來(lái),如果采用了 Common Lisp 的解決方案,那么名字沖突就太可怕了,隨便給私有成員變量起個(gè)名字就可能跟某個(gè)上層基類的同名成員相沖突,這顯然是不好的。

  后記

  敝人的 Java 純屬初學(xué),以上關(guān)于 Java 特性的描述如有失當(dāng)之處,希望有關(guān)讀者予以指出,深表謝意。