歷代的程式語言演化,都嘗試著在解決不同的附屬性困難問題。從Java反省C++的諸般問題而進行改良的結果,我們可以看出它所嘗試要解決的困難。

為什麼Java要採用垃圾收集機制?
Java中有一個很重大的特色,就是所謂的垃圾收集機制(Garbage Collection,GC)。垃圾收集機制允許程式設計者儘管放心的去配置所需的記憶體空間,而毋需自行處理這些記憶體空間的釋放問題,因為所謂的垃圾收集器(Garbage Collector)會自動判斷已經不被程式所使用的記憶體空間,並且自動釋放。

慣用C/C++程式語言的程式設計者都知道,自己配置的記憶體必須自己妥善釋放,無論是沒有釋放或是重複釋放,都會造成程式的臭蟲。程式設計者自己配置記憶體、自己釋放是一種程式設計模型,而只管配置、不管釋放,又是另一種程式設計模型。

Java繼承了C/C++的血統,卻選擇了另外一種截然不同處理記憶體配置與釋放的程式設計模型,為什麼呢?在Java問世之前,垃圾收集機制已經存在,但鮮少看到十分主流的程式語言採用,原因就是在於它的效能問題。因為自動判斷每一塊記憶體區塊是否該被回收及加以回收,都需要不少計算量,因而影響到程式的效能。而Java發明者決心以效能換得這個垃圾收集機制,是一個重大的決定,為什麼呢?這自然是因為讓程式設計者自己配置記憶體、自己釋放的程式設計模型,存在著影響深遠的問題,容易滋生出程式臭蟲。

從指標與陣列來看C/C++
除了記憶體配置與釋放的問題之外,還有許多問題長期以來困擾著C/C++的程式設計者,例如指標的問題。

指標是C/C++中十分具有威力的語法元素,運用指標可以達到許多神奇的效果,也能寫出相當優雅的程式碼。指標的運用,可以說是C/C++語言中的一門藝術。但是不幸的,水能載舟亦能覆舟,指標相較於記憶體配置與釋放的問題,更是容易成為臭蟲的溫床。相信有足夠C/C++經驗的程式設計者,都能理解指標的誤用、錯用,十分容易引發程式的臭蟲,而且相當不易察覺。指標允許你指向記憶體的任意位置,但是一旦指向不屬於應用程式可存取的記憶體位址範圍、或是以不正確的方式解讀所指向記憶體中的資料,就會引發嚴重的錯誤。

而陣列的越位存取也是和指標類似的問題,這一類的問題,甚至可以被刻意操作,衍生成為緩衝區溢位攻擊。這類的問題,只要有過足夠C/C++程式設計經驗的程式設計者,應當都能體會這些在程式設計過程中所可能造成的困擾。它們不僅容易造成臭蟲滋生,而且所長出來的臭蟲有很高的比例,都會藏在程式中最隱晦的角落,讓你難以發覺。

效果強與易犯錯之間如何取捨?
C/C++的程式設計者在記憶體處理的動作上,得到極為高度的自由,但又吃盡苦頭。C/C++在記憶體處理的程式設計模型很有威力,卻也容易讓程式設計者犯錯,而且一旦犯了錯,程式設計者必須花費數倍、甚至數十倍撰寫出該錯誤的心力和時間,才能揪出這個錯誤並加以修正。這當然是一個值得思考的問題,我們是否要在一個語言中,放入一個極有威力、卻容易造成程式設計者犯錯的程式模型?

顯然Java的發明者選擇不這麼做,他寧可放棄有威力的武器,也不希望這個武器在傷敵的同時,也同樣傷到自己。

而這也反映Java語言在眾多面向上的設計哲學,寧可削除一些看似彈性、看似有威力的語法元素,也要換取降低程式設計者因而犯錯的機會。所以,Java才會得到一個 “C++ --“ 的評語。身為一個後繼、流著相同血源的程式語言,卻試圖削減,可以想見其中一定充滿著許多的反省。

所以,我們可以看見Java中取消了指標,改用參照(reference)來指向物件,沒有指標指向任意記憶體位址的能力。一旦程式設計者做了越界的陣列操作,Java的虛擬機器就會擲出異常,並且不允許執行該操作。同樣的,程式設計者也無法對指向null的空參照值進行存取,一樣會受到虛擬機器的控管,擲出對應的異常物件,待應用程式處理。

從生產力來看,品質才是關鍵
上述這幾個例子,都是傳統C/C++程式設計容易犯錯的地方,Java從語言的根本上嘗試杜絕這幾類問題的發生。這些在語言上的設計,也都反映Java的基本哲學──減少程式設計者犯錯的機會。

為什麼Java這麼重視這件事?因為如此才能大大提升軟體開發時的生產力。事實上的確如此,正如前段所提的,一旦寫下了一個錯誤,就必須花費數倍、甚至數十倍撰寫出該錯誤的心力和時間,才能揪出這個錯誤並加以修正。你可以想像,這樣開發對生產力會有多大的影響。

花多少時間寫下程式碼或許是生產力的關鍵之一,但是,總共花多少時間得到足夠品質的程式碼,才是最後所展現出來的真正生產力指標。我們可以說,Java是把生產力當做最高指導原則之一的程式語言,而它想要得到生產力的重要途徑之一,就是在語言層次上盡量避免程式設計者犯錯。

C和C++這兩個語言中,存在不少容易讓程式設計者犯錯的地方,這是Java之所以要C加加之後再減減的原因。

舉例來說:
int a = 3;
int b = 1;
if( a = b ) // if( a == b )
{
// 執行一些動作
}

這樣的寫法無論在C或C++裡都是合法的,但是,你有沒有注意到,在if的條件式中,程式設計者有可能少打了一個 = 號,他原先想寫下的條件式可能是 a == b,但因為疏失寫成了 a = b,而這仍然是可通過編譯的C/C++程式碼,它的語義從原先的若a值等於b值則條件成立,變成了將b值指派給變數a,若a在指派後的值大於0,則條件成立。

相信寫過C/C++的程式設計者都可能犯下這樣子的錯誤,而且也都能理解這樣子的錯單純只是因為輸入程式碼的錯誤,卻有可能花上你許多的時間才能找到。從語法上來看,這是從C時代就具備的語法,它很強大、很有彈性,可以寫出很取巧的程式碼,但是也留下了讓程式設計者犯下上述錯誤的空間。當然,這樣子的語法到了Java,已經不允許了。

你可以在C++中,找到更多更有威力、更有彈性的語法,程式設計者得以憑藉來達成很厲害的效果,但是,使用它們要更小心、更謹慎、甚至需要對語言更了解,也更需要技巧,才能將它們運用地淋漓盡致,而且,一不小心,就會犯下難以修正的錯誤。而其中並不是難在糾正本身,而是難在找到為什麼而錯。

像C++中的運算子重載,基本上在Java程式語言中是沒有的,而像C++中的多重繼承,在Java中也僅提供多重介面的實作。從這種角度來看,你可以說Java比較弱,但這是透過刻意削減容易出錯的語言元素,來減少程式設計者犯錯的機會,即使語言威力變弱了、看起來不那麼巧妙了、或許要用比較多的程式碼才能做到一樣的事,而程式碼或許看起來很笨,但因為不容易犯錯,反而可以提高生產力。

這就是Java嘗試提高生產力的一個重要方向。

 

專欄作者

熱門新聞

Advertisement