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語言基礎相關的問題及建議的解決方案和變量的注意事項、如何安全的序列化、斷言到底該如何使用等;
包名全小寫,類名首字母全大寫,常量全部大寫並用下劃線分隔,變量采用駝峰命名法(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"作為長整型標志時務必大寫。
常量蛻變成變量?你胡扯吧,加了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是常量嗎?它的值會變嗎?絕對會變!這種常量的定義方式是絕對不可取的,常量就是常量,在編譯期就必須確定其值,不應該在運行期更改,否則程序的可讀性會非常差,甚至連作者自己都不能確定在運行期發生了何種神奇的事情。
甭想著使用常量會變的這個功能來實現序列號算法、隨機種子生成,除非這真的是項目中的唯一方案,否則就放棄吧,常量還是當常量使用。
注意:務必讓常量的值在運行期保持不變。
三元操作符是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不相等了。這裡為什麼是整型轉成浮點型,而不是浮點型轉成整型呢?這就涉及三元操作符類型的轉換規則:
知道什麼原因了,相應的解決辦法也就有了:保證三元操作符中的兩個操作數類型一致,避免此錯誤的發生。
在項目和系統開發中,為了提高方法的靈活度和可復用性,我們經常要傳遞不確定數量的參數到方法中,在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是一個原生數據類型,而數組本身是一個對象,編譯器想要"偷懶",於是它會從最簡單的開始"猜想",只要符合編譯條件的即可通過,於是就出現了此問題。
問題闡述清楚了,為了讓我們的程序能被"人類"看懂,還是慎重考慮變長參數的方法重載吧,否則讓人傷腦筋不說,說不定哪天就陷入這類小陷阱裡了。
上一建議講解了變長參數的重載問題,本建議會繼續討論變長參數的重載問題,上一建議的例子是變長參數的范圍覆蓋了非變長參數的范圍,這次討論兩個都是變長參數的方法說起,代碼如下:
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類型的,編譯即可順利通過,也就減少了錯誤的發生。