編寫高質量程式碼:改善Java程式的151個建議(第1章:JAVA開發中通用的方法和準則___建議1~5)

阿赫瓦里發表於2016-09-08

             The reasonable man adapts himself to the world; The unreasonable one persists in trying to adapt the world himself.

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

                                -------蕭伯納

  本系類文章,用來記錄《編寫高質量程式碼 改善java程式的151個建議》這本書的讀書筆記。方便自己檢視,也方便大家查閱,在此感謝原書作者秦小波對java的獨特見解,幫助java愛好者的成長。由於篇幅原因本人將讀書筆記採取分批記憶的方式來進行記錄分享,全書共12章,共有151條建議,其中1~3章針對java語法本身提出了51條建議;第4~9章重點針對JDK API的使用提出了80條建議;第10~12章針對程式的效能、開源的工具和框架、編碼風格和程式設計思想等方面提出了20條建議。本人根據此書的目錄結構,循序漸進的閱讀此書,特記錄於此。

  本人第一次閱讀本書是1年半以前,當時看的不仔細,只是瞭解了一些問題,當時覺得這本書還可以,現在1年後又將他翻出來,重新開始看,覺得理解還是和第一次看的韻味有不同之處,所以建議大家看書時一定要細心的理解去看,不可追尋速度,要仔細品味,因為讀多少書,不是用來出去吹牛逼的,我都讀了哪些哪些書,用了很短的時間之外的.....   書是讀給自己的,用來提升自我的,所以要細心理解,思考至上,學以致用。技術類的書籍大同小異,原理不變,所以用心讀完一本技術的書籍,以後再讀其他關於此類技術的書籍,你的理解會更深刻一些,相比快速的讀了若干本關於一方面技術的書籍,最後還是一知半解,只知其然,不知其所以然。

  JAVA的世界豐富又多彩,但同時也佈滿了經濟陷阱,大家一不小心就可能跌入黑暗的深淵,只有在瞭解了其通行規則後才能是自己在技術的海洋裡遨遊飛翔,恣意馳騁。千里之行,始於足下,本章主要講述與JAVA語言基礎相關的問題及建議的解決方案和變數的注意事項、如何安全的序列化、斷言到底該如何使用等;

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

    包名全小寫,類名首字母全大寫,常量全部大寫並用下劃線分隔,變數採用駝峰命名法(Camel Case)命名等,這些都是最基本的Java編碼規範,是每個javaer都應熟知的規則,但是在變數的宣告中要注意不要引入容易混淆的字母。嘗試閱讀如下程式碼,思考列印結果的i是多少:

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

      肯定會有人說:這麼簡單的例子還能出錯?執行結果肯定是22!實踐是檢驗真理的唯一標準,將其Run一下看看,或許你會很奇怪,結果是2,而不是22.難道是編譯器出問題了,少了個"2"?

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

注意:字母"l"作為長整型標誌時務必大寫

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

  常量蛻變成變數?你胡扯吧,加了final和static的常量怎麼可能會變呢?不可能為此賦值的呀。真的不可能嗎?看看如下程式碼:

 1 import java.util.Random;
 2 
 3 public class Demo01 {
 4     public static void main(String[] args) {
 5         test02();
 6     }
 7 
 8     public static void test02() {
 9         System.out.println("常量會變哦:" + Constant.RAND_CONST);
10     }
11 }
12 
13 interface Constant {
14     public static final int RAND_CONST = new Random().nextInt();
15 }

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

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

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

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

 三元操作符是if-else的簡化寫法,在專案中使用它的地方很多,也非常好用,但是好用又簡單的東西並不表示就可以隨意使用,看看如下程式碼:

1 public static void test03() {
2         int i = 80;
3         String str = String.valueOf(i < 100 ? 90 : 100);
4         String str1 = String.valueOf(i < 100 ? 90 : 100.0);
5         System.out.println("兩者是否相等:" + str.equals(str1));
6     }

     分析一下這段程式,i是80,小於100,兩者的返回值肯定都是90,再轉成String型別,其值也絕對相等,毋庸置疑的。嗯,分析的有點道理,但是變數str中的三元操作符的第二個運算元是100,而str1中的第二個運算元是100.0,難道木有影響嗎?不可能有影響吧,三元操作符的條件都為真了,只返回第一個值嘛,於第二個值有毛線關係,貌似有道理。

  執行之後,結果卻是:"兩者是否相等:false",不相等,why?

  問題就出在了100和100.0這兩個數字上,在變數str中,三元操作符的第一個運算元90和第二個運算元100都是int型別,型別相同,返回的結果也是int型別的90,而變數str1中的第一個運算元(90)是int型別,第二個運算元100.0是浮點數,也就是兩個運算元的型別不一致,可三元操作符必須要返回一個資料,而且型別要確定,不可能條件為真時返回int型別,條件為假時返回float型別,編譯器是不允許如此的,所以它會進行型別轉換int型別轉換為浮點數90.0,也就是三元操作符的返回值是浮點數90.0,那麼當然和整型的90不相等了。這裡為什麼是整型轉成浮點型,而不是浮點型轉成整型呢?這就涉及三元操作符型別的轉換規則:

  1.   若兩個運算元不可轉換,則不作轉換,返回值是Object型別;
  2.   若兩個運算元是明確型別的表示式(比如變數),則按照正常的二進位制數字轉換,int轉為long,long轉為float等;
  3.   若兩個運算元中有一個是數字S,另外一個是表示式,且其型別標誌位T,那麼,若數字S在T的範圍內,則轉換為T型別;若S超出了T的範圍,則T轉換為S;
  4.   若兩個運算元都是直接量數字,則返回值型別範圍較大者。

  知道什麼原因了,相應的解決辦法也就有了:保證三元操作符中的兩個運算元型別一致,避免此錯誤的發生。

建議4:避免帶有變長引數的方法過載

  在專案和系統開發中,為了提高方法的靈活度和可複用性,我們經常要傳遞不確定數量的引數到方法中,在JAVA5之前常用的設計技巧就是把形參定義成Collection型別或其子類型別,或者陣列型別,這種方法的缺點就是需要對空引數進行判斷和篩選,比如實參為null值和長度為0的Collection或陣列。而Java5引入了變長引數(varags)就是為了更好地挺好方法的複用性,讓方法的呼叫者可以"隨心所欲"地傳遞實引數量,當然變長引數也是要遵循一定規則的,比如變長引數必須是方法中的最後一個引數;一個方法不能定義多個變長引數等,這些基本規則需要牢記,但是即使記住了這些規則,仍然有可能出現錯誤,看如下程式碼:

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

  這是一個計算商品折扣的模擬類,帶有兩個引數的calPrice方法(該方法的業務邏輯是:提供商品的原價和折扣率,即可獲得商品的折扣價)是一個簡單的折扣計算方法,該方法在實際專案中經常會用到,這是單一的打折方法。而帶有變長引數的calPrice方法是叫較複雜的折扣計算方式,多種折扣的疊加運算(模擬類是比較簡單的實現)在實際中也經常見到,比如在大甩賣期間對VIP會員再度進行打折;或者當天是你的生日,再給你打個9折,也就是俗話中的折上折。

  業務邏輯清楚了,我們來仔細看看這兩個方法,它們是過載嗎?當然是了,過載的定義是:"方法名相同,引數型別或數量不同",很明顯這兩個方法是過載。但是這個過載有點特殊,calPrice(int price ,int... discounts)的引數範疇覆蓋了calPrice(int price,int discount)的引數範疇。那問題就出來了:對於calPrice(499,75)這樣的計算,到底該呼叫哪個方法來處理呢?

  我們知道java編譯器是很聰明的,它在編譯時會根據方法簽名來確定呼叫那個方法,比如:calPrice(499,75,95)這個呼叫,很明顯75和95會被轉成一個包含兩個元素的陣列,並傳遞到calPrice(int price,int...discounts)中,因為只有這一個方法符合這個實參型別,這很容易理解。但是我們現在面對的是calPrice(499,75)呼叫,這個75既可以被編譯成int型別的75,也可以被編譯成int陣列{75},即只包含一個元素的陣列。那到底該呼叫哪一個方法呢?執行結果是:"簡單折扣後的價格是:374.25"。看來呼叫了第一個方法,為什麼會呼叫第一個方法,而不是第二個變長方法呢?因為java在編譯時,首先會根據實參的數量和型別(這裡2個實參,都為int型別,注意沒有轉成int陣列)來進行處理,也就是找到calPrice(int price,int discount)方法,而且確認他是否符合方法簽名條件。現在的問題是編譯器為什麼會首先根據兩個int型別的實參而不是一個int型別,一個int陣列型別的實參來查詢方法呢?

  因為int是一個原生資料型別,而陣列本身是一個物件,編譯器想要"偷懶",於是它會從最簡單的開始"猜想",只要符合編譯條件的即可通過,於是就出現了此問題。

  問題闡述清楚了,為了讓我們的程式能被"人類"看懂,還是慎重考慮變長引數的方法過載吧,否則讓人傷腦筋不說,說不定哪天就陷入這類小陷阱裡了。

建議5:別讓null值和空值威脅到變長方法  

 上一建議講解了變長引數的過載問題,本建議會繼續討論變長引數的過載問題,上一建議的例子是變長引數的範圍覆蓋了非變長引數的範圍,這次討論兩個都是變長引數的方法說起,程式碼如下:

 1 public class Client5 {
 2 
 3     public void methodA(String str, Integer... is) {
 4 
 5     }
 6 
 7     public void methodA(String str, String... strs) {
 8 
 9     }
10 
11     public static void main(String[] args) {
12         Client5 client5 = new Client5();
13         client5.methodA("china", 0);
14         client5.methodA("china", "people");
15         client5.methodA("china");
16         client5.methodA("china", null);
17     }
18 }

  兩個methodA都進行了過載,現在的問題是:上面的client5.methodA("china");client5.methodA("china", null);編譯不通過,提示相同:方法模糊不清,編譯器不知道呼叫哪一個方法,但這兩處程式碼反應的味道是不同的。

  對於methodA("china")方法,根據實參"china"(String型別),兩個方法都符合形參格式,編譯器不知道呼叫那個方法,於是報錯。我們思考一下此問題:Client5這個類是一個複雜的商業邏輯,提供了兩個過載方法,從其它模組呼叫(系統內本地呼叫系統或系統外遠端系統呼叫)時,呼叫者根據變長引數的規範呼叫,傳入變長引數的引數數量可以是N個(N>=0),那當然可以寫成client5.methodA("china")方法啊!完全符合規範,但是這個卻讓編譯器和呼叫者鬱悶,程式符合規則卻不能執行,如此問題,誰之責任呢?是Client5類的設計者,他違反了KISS原則(Keep it Smile,Stupid,即懶人原則),按照此設計的方法應該很容一呼叫,可是現在遵循規範卻編譯不通過,這對設計者和開發者而言都是應該禁止出現的。

  對於Client5.methodA("China",null),直接量null是沒喲型別的,雖然兩個methodA方法都符合呼叫要求,但不知道呼叫哪一個,於是報錯了。仔細分析一下,除了不符合上面的懶人原則之外,還有一個非常不好的編碼習慣,即呼叫者隱藏了實參型別,這是非常危險的,不僅僅呼叫者需要"猜測呼叫那個方法",而且被呼叫者也可能產生內部邏輯混亂的情況。對於本例來說應該如此修改:

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

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

相關文章