編寫高質量代碼:改善Java程序的151個建議(1)
第1章 Java開發中通用的方法和準則
Thereasonablemanadaptshimselftotheworld;theunreasonableonepersistsintryingtoadapttheworldtohimself.
明白事理的人使自己適應世界;不明事理的人想讓世界適應自己。
—蕭伯納
Java的世界豐富又多彩,但同時也布滿了荊棘陷阱,大家一不小心就可能跌入黑暗深淵,只有在了解了其通行規則后才能使自己在技術的海洋里遨游飛翔,恣意馳騁。
“千里之行始于足下”,本章主要講述與Java語言基礎有關的問題及建議的解決方案,例如常量和變量的注意事項、如何更安全地序列化、斷言到底該如何使用等。
建議1:不要在常量和變量中出現易混淆的字母
包名全小寫,類名首字母全大寫,常量全部大寫并用下劃線分隔,變量采用駝峰命名法(CamelCase)命名等,這些都是最基本的Java編碼規范,是每個Javaer都應熟知的規則,但是在變量的聲明中要注意不要引入容易混淆的字母。嘗試閱讀如下代碼,思考一下打印出的i等于多少:
|
肯定有人會說:這么簡單的例子還能出錯?運行結果肯定是22!實踐是檢驗真理的唯一標準,將其拷貝到Eclipse中,然后Run一下看看,或許你會很奇怪,結果是2,而不是22,難道是Eclipse的顯示有問題,少了個“2”?
因為賦給變量i的數字就是“1”,只是后面加了長整型變量的標示字母“l”而已。別說是我挖坑讓你跳,如果有類似程序出現在項目中,當你試圖通過閱讀代碼來理解作者的思想時,此情此景就有可能會出現。所以,為了讓您的程序更容易理解,字母“l”(還包括大寫字母“O”)盡量不要和數字混用,以免使閱讀者的理解與程序意圖產生偏差。如果字母和數字必須混合使用,字母“l”務必大寫,字母“O”則增加注釋。
注意 字母“l”作為長整型標志時務必大寫。
建議2:莫讓常量蛻變成變量
常量蛻變成變量?你胡扯吧,加了final和static的常量怎么可能會變呢?不可能二次賦值的呀。真的不可能嗎?看我們神奇的魔術,代碼如下:
|
RAND_CONST是常量嗎?它的值會變嗎?絕對會變!這種常量的定義方式是極不可取的,常量就是常量,在編譯期就必須確定其值,不應該在運行期更改,否則程序的可讀性會非常差,甚至連作者自己都不能確定在運行期發生了何種神奇的事情。
甭想著使用常量會變的這個功能來實現序列號算法、隨機種子生成,除非這真的是項目中的唯一方案,否則就放棄吧,常量還是當常量使用。
注意:務必讓常量的值在運行期保持不變。
建議3:三元操作符的類型務必一致
三元操作符是if-else的簡化寫法,在項目中使用它的地方很多,也非常好用,但是好用又簡單的東西并不表示就可以隨便用,我們來看看下面這段代碼:
|
分析一下這段程序:i是80,那它當然小于100,兩者的返回值肯定都是90,再轉成String類型,其值也絕對相等,毋庸置疑的。恩,分析得有點道理,但是變量s中三元操作符的第二個操作數是100,而s1的第二個操作數是100.0,難道沒有影響嗎?不可能有影響吧,三元操作符的條件都為真了,只返回第一個值嘛,與第二個值有一毛錢的關系嗎?貌似有道理。
果真如此嗎?我們通過結果來驗證一下,運行結果是:“兩者是否相等:false”,什么?不相等,Why?
問題就出在了100和100.0這兩個數字上,在變量s中,三元操作符中的第一個操作數(90)和第二個操作數(100)都是int類型,類型相同,返回的結果也就是int類型的90,而變量s1的情況就有點不同了,第一個操作數是90(int類型),第二個操作數卻是100.0,而這是個浮點數,也就是說兩個操作數的類型不一致,可三元操作符必須要返回一個數據,而且類型要確定,不可能條件為真時返回int類型,條件為假時返回float類型,編譯器是不允許如此的,所以它就會進行類型轉換了,int型轉換為浮點數90.0,也就是說三元操作符的返回值是浮點數90.0,那這當然與整型的90不相等了。這里可能有讀者疑惑了:為什么是整型轉為浮點,而不是浮點轉為整型呢?這就涉及三元操作符類型的轉換規則:
若兩個操作數不可轉換,則不做轉換,返回值為Object類型。
若兩個操作數是明確類型的表達式(比如變量),則按照正常的二進制數字來轉換,int類型轉換為long類型,long類型轉換為float類型等。
若兩個操作數中有一個是數字S,另外一個是表達式,且其類型標示為T,那么,若數字S在T的范圍內,則轉換為T類型;若S超出了T類型的范圍,則T轉換為S類型(可以參考“建議22”,會對該問題進行展開描述)。
若兩個操作數都是直接量數字(Literal),則返回值類型為范圍較大者。
知道是什么原因了,相應的解決辦法也就有了:保證三元操作符中的兩個操作數類型一致,即可減少可能錯誤的發生。
建議4:避免帶有變長參數的方法重載
在項目和系統的開發中,為了提高方法的靈活度和可復用性,我們經常要傳遞不確定數量的參數到方法中,在Java 5之前常用的設計技巧就是把形參定義成Collection類型或其子類類型,或者是數組類型,這種方法的缺點就是需要對空參數進行判斷和篩選,比如實參為null值和長度為0的Collection或數組。而 Java 5引入變長參數(varags)就是為了更好地提高方法的復用性,讓方法的調用者可以“隨心所欲”地傳遞實參數量,當然變長參數也是要遵循一定規則的,比如變長參數必須是方法中的最后一個參數;一個方法不能定義多個變長參數等,這些基本規則需要牢記,但是即使記住了這些規則,仍然有可能出現錯誤,我們來看如下代碼:
|
這是一個計算商品價格折扣的模擬類,帶有兩個參數的calPrice方法(該方法的業務邏輯是:提供商品的原價和折扣率,即可獲得商品的折扣價)是一個簡單的折扣計算方法,該方法在實際項目中經常會用到,這是單一的打折方法。而帶有變長參數的calPrice方法則是較復雜的折扣計算方式,多種折扣的疊加運算(模擬類是一種比較簡單的實現)在實際生活中也是經常見到的,比如在大甩賣期間對VIP會員再度進行打折;或者當天是你的生日,再給你打個9折,也就是俗話說的“折上折”。
業務邏輯清楚了,我們來仔細看看這兩個方法,它們是重載嗎?當然是了,重載的定義是“方法名相同,參數類型或數量不同”,很明顯這兩個方法是重載。但是再仔細瞧瞧,這個重載有點特殊:calPrice(int price,int... discounts)的參數范疇覆蓋了calPrice(int price,int discount)的參數范疇。那問題就出來了:對于calPrice(49900,75)這樣的計算,到底該調用哪個方法來處理呢?
我們知道Java編譯器是很聰明的,它在編譯時會根據方法簽名(Method Signature)來確定調用哪個方法,比如calPrice(499900,75,95)這個調用,很明顯75和95會被轉成一個包含兩個元素的數組,并傳遞到calPrice(int price,in.. discounts)中,因為只有這一個方法簽名符合該實參類型,這很容易理解。但是我們現在面對的是calPrice(49900,75)調用,這個“75”既可以被編譯成int類型的“75”,也可以被編譯成int數組“{75}”,即只包含一個元素的數組。那到底該調用哪一個方法呢?我們先運行一下看看結果,運行結果是:
簡單折扣后的價格是:¥374.25。
看來是調用了第一個方法,為什么會調用第一個方法,而不是第二個變長參數方法呢?因為Java在編譯時,首先會根據實參的數量和類型(這里是2個實參,都為int類型,注意沒有轉成int數組)來進行處理,也就是查找到calPrice(int price,int discount)方法,而且確認它是否符合方法簽名條件。現在的問題是編譯器為什么會首先根據2個int類型的實參而不是1個int類型、1個int數組類型的實參來查找方法呢?這是個好問題,也非常好回答:因為int是一個原生數據類型,而數組本身是一個對象,編譯器想要“偷懶”,于是它會從最簡單的開始“猜想”,只要符合編譯條件的即可通過,于是就出現了此問題。
問題是闡述清楚了,為了讓我們的程序能被“人類”看懂,還是慎重考慮變長參數的方法重載吧,否則讓人傷腦筋不說,說不定哪天就陷入這類小陷阱里了。
建議5:別讓null值和空值威脅到變長方法
上一建議講解了變長參數的重載問題,本建議還會繼續討論變長參數的重載問題。上一建議的例子是變長參數的范圍覆蓋了非變長參數的范圍,這次我們從兩個都是變長參數的方法說起,代碼如下:
|
兩個methodA都進行了重載,現在的問題是:上面的代碼編譯通不過,問題出在什么地方?看似很簡單哦。
有兩處編譯通不過:client.methodA("China")和client.methodA("China",null),估計你已經猜到了,兩處的提示是相同的:方法模糊不清,編譯器不知道調用哪一個方法,但這兩處代碼反映的代碼味道可是不同的。
對于methodA("China")方法,根據實參“China”(String類型),兩個方法都符合形參格式,編譯器不知道該調用哪個方法,于是報錯。我們來思考這個問題:Client類是一個復雜的商業邏輯,提供了兩個重載方法,從其他模塊調用(系統內本地調用或系統外遠程調用)時,調用者根據變長參數的規范調用, 傳入變長參數的實參數量可以是N個(N>=0),那當然可以寫成client.methodA("china")方法啊!完全符合規范,但是這卻讓編譯器和調用者都很郁悶,程序符合規則卻不能運行,如此問題,誰之責任呢?是Client類的設計者,他違反了KISS原則(Keep It Simple, Stupid,即懶人原則),按照此規則設計的方法應該很容易調用,可是現在在遵循規范的情況下,程序竟然出錯了,這對設計者和開發者而言都是應該嚴禁出現的。
對于client.methodA("china",null)方法,直接量null是沒有類型的,雖然兩個methodA方法都符合調用請求,但不知道調用哪一個,于是報錯了。我們來體會一下它的壞味道:除了不符合上面的懶人原則外,這里還有一個非常不好的編碼習慣,即調用者隱藏了實參類型,這是非常危險的,不僅僅調用者需要“猜測”該調用哪個方法,而且被調用者也可能產生內部邏輯混亂的情況。對于本例來說應該做如下修改:
|
也就是說讓編譯器知道這個null值是String類型的,編譯即可順利通過,也就減少了錯誤的發生。