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