qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          編寫高質量代碼:改善Java程序的151個建議(1)

           第1章 Java開發中通用的方法和準則

            Thereasonablemanadaptshimselftotheworld;theunreasonableonepersistsintryingtoadapttheworldtohimself.

            明白事理的人使自己適應世界;不明事理的人想讓世界適應自己。

            —蕭伯納

            Java的世界豐富又多彩,但同時也布滿了荊棘陷阱,大家一不小心就可能跌入黑暗深淵,只有在了解了其通行規則后才能使自己在技術的海洋里遨游飛翔,恣意馳騁。

            “千里之行始于足下”,本章主要講述與Java語言基礎有關的問題及建議的解決方案,例如常量和變量的注意事項、如何更安全地序列化、斷言到底該如何使用等。

            建議1:不要在常量和變量中出現易混淆的字母

            包名全小寫,類名首字母全大寫,常量全部大寫并用下劃線分隔,變量采用駝峰命名法(CamelCase)命名等,這些都是最基本的Java編碼規范,是每個Javaer都應熟知的規則,但是在變量的聲明中要注意不要引入容易混淆的字母。嘗試閱讀如下代碼,思考一下打印出的i等于多少:

          1. public class Client {  
          2.      public static void main(String[] args) {  
          3.            long i = 1l;  
          4.            System.out.println("i的兩倍是:" + (i+i));  
          5.      }  
          6. }

            肯定有人會說:這么簡單的例子還能出錯?運行結果肯定是22!實踐是檢驗真理的唯一標準,將其拷貝到Eclipse中,然后Run一下看看,或許你會很奇怪,結果是2,而不是22,難道是Eclipse的顯示有問題,少了個“2”?

            因為賦給變量i的數字就是“1”,只是后面加了長整型變量的標示字母“l”而已。別說是我挖坑讓你跳,如果有類似程序出現在項目中,當你試圖通過閱讀代碼來理解作者的思想時,此情此景就有可能會出現。所以,為了讓您的程序更容易理解,字母“l”(還包括大寫字母“O”)盡量不要和數字混用,以免使閱讀者的理解與程序意圖產生偏差。如果字母和數字必須混合使用,字母“l”務必大寫,字母“O”則增加注釋。

            注意 字母“l”作為長整型標志時務必大寫。

            建議2:莫讓常量蛻變成變量

            常量蛻變成變量?你胡扯吧,加了final和static的常量怎么可能會變呢?不可能二次賦值的呀。真的不可能嗎?看我們神奇的魔術,代碼如下:

          1. public class Client {     
          2.      public static void main(String[] args) {  
          3.            System.out.println("常量會變哦:" + Const.RAND_CONST);  
          4.      }  
          5. }  
          6. /*接口常量*/  
          7. interface Const{  
          8.     //這還是常量嗎?  
          9.     public static final int RAND_CONST = new Random().nextInt();  
          10. }

            RAND_CONST是常量嗎?它的值會變嗎?絕對會變!這種常量的定義方式是極不可取的,常量就是常量,在編譯期就必須確定其值,不應該在運行期更改,否則程序的可讀性會非常差,甚至連作者自己都不能確定在運行期發生了何種神奇的事情。

            甭想著使用常量會變的這個功能來實現序列號算法、隨機種子生成,除非這真的是項目中的唯一方案,否則就放棄吧,常量還是當常量使用。

            注意:務必讓常量的值在運行期保持不變。

          建議3:三元操作符的類型務必一致

            三元操作符是if-else的簡化寫法,在項目中使用它的地方很多,也非常好用,但是好用又簡單的東西并不表示就可以隨便用,我們來看看下面這段代碼:

          1. public class Client {  
          2.      public static void main(String[] args) {  
          3.            int i = 80;  
          4.            String s = String.valueOf(i<100?90:100);  
          5.            String s1 = String.valueOf(i<100?90:100.0);  
          6.            System.out.println("兩者是否相等:"+s.equals(s1));  
          7.      }  
          8. }

            分析一下這段程序: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)就是為了更好地提高方法的復用性,讓方法的調用者可以“隨心所欲”地傳遞實參數量,當然變長參數也是要遵循一定規則的,比如變長參數必須是方法中的最后一個參數;一個方法不能定義多個變長參數等,這些基本規則需要牢記,但是即使記住了這些規則,仍然有可能出現錯誤,我們來看如下代碼:

          1. public class Client {     
          2.      //簡單折扣計算  
          3.      public void calPrice(int price,int discount){  
          4.            float knockdownPrice =price * discount / 100.0F;  
          5.            System.out.println("簡單折扣后的價格是:"+formateCurrency(knockdownPrice));  
          6.      }    
          7.      //復雜多折扣計算  
          8.      public void calPrice(int price,int... discounts){  
          9.            float knockdownPrice = price;  
          10.            for(int discount:discounts){  
          11.                    knockdownPriceknockdownPrice = knockdownPrice * discount / 100;  
          12.         }  
          13.         System.out.println("復雜折扣后的價格是:" +formateCurrency(knockdownPrice));  
          14.      }  
          15.      //格式化成本的貨幣形式  
          16.      private String formateCurrency(float price){  
          17.             return NumberFormat.getCurrencyInstance().format(price/100);  
          18.      }  
          19.       
          20.      public static void main(String[] args) {  
          21.            Client client = new Client();  
          22.            //499元的貨物,打75折  
          23.            client.calPrice(49900, 75);  
          24.     }  
          25. }

            這是一個計算商品價格折扣的模擬類,帶有兩個參數的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值和空值威脅到變長方法

            上一建議講解了變長參數的重載問題,本建議還會繼續討論變長參數的重載問題。上一建議的例子是變長參數的范圍覆蓋了非變長參數的范圍,這次我們從兩個都是變長參數的方法說起,代碼如下:

          1. public class Client {  
          2.      public void methodA(String str,Integer... is){       
          3.      }  
          4.       
          5.      public void methodA(String str,String... strs){          
          6.      }  
          7.       
          8.      public static void main(String[] args) {  
          9.            Client client = new Client();  
          10.            client.methodA("China", 0);  
          11.            client.methodA("China", "People");  
          12.            client.methodA("China");  
          13.            client.methodA("China",null);  
          14.      }  
          15. }

            兩個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方法都符合調用請求,但不知道調用哪一個,于是報錯了。我們來體會一下它的壞味道:除了不符合上面的懶人原則外,這里還有一個非常不好的編碼習慣,即調用者隱藏了實參類型,這是非常危險的,不僅僅調用者需要“猜測”該調用哪個方法,而且被調用者也可能產生內部邏輯混亂的情況。對于本例來說應該做如下修改:

          1. public static void main(String[] args) {  
          2.      Client client = new Client();  
          3.      String[] strs = null;  
          4.      client.methodA("China",strs);  
          5. }

            也就是說讓編譯器知道這個null值是String類型的,編譯即可順利通過,也就減少了錯誤的發生。

          posted on 2012-02-17 16:46 順其自然EVO 閱讀(391) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          <2012年2月>
          2930311234
          567891011
          12131415161718
          19202122232425
          26272829123
          45678910

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 双鸭山市| 兖州市| 锡林浩特市| 昌邑市| 上栗县| 内江市| 闽清县| 黎平县| 延津县| 万州区| 玛纳斯县| 绥化市| 岳池县| 大足县| 双柏县| 惠来县| 新竹县| 和田市| 青神县| 钟祥市| 东方市| 敦化市| 清河县| 额敏县| 合江县| 调兵山市| 墨脱县| 肥东县| 曲松县| 西乌珠穆沁旗| 上蔡县| 黔西| 育儿| 永新县| 湛江市| 松桃| 怀远县| 黄浦区| 惠安县| 当阳市| 招远市|