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

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

ase 重載方法 name 原理 .get tin stat eas 容易

閱讀目錄

  • 建議1:不要在常量和變量中出現易混淆的字母
  • 建議2:莫讓常量蛻變成變量   
  • 建議3:三元操作符的類型務必一致  
  • 建議4:避免帶有變長參數的方法重載
  • 建議5:別讓null值和空值威脅到變長方法  

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類型的,編譯即可順利通過,也就減少了錯誤的發生。

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