




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
\h編寫高質(zhì)量代碼改善Java程序的151個(gè)建議目錄\h第1章Java開發(fā)中通用的方法和準(zhǔn)則\h建議1:不要在常量和變量中出現(xiàn)易混淆的字母\h建議2:莫讓常量蛻變成變量\h建議3:三元操作符的類型務(wù)必一致\h建議4:避免帶有變長參數(shù)的方法重載\h建議5:別讓null值和空值威脅到變長方法\h建議6:覆寫變長方法也循規(guī)蹈矩\h建議7:警惕自增的陷阱\h建議8:不要讓舊語法困擾你\h建議9:少用靜態(tài)導(dǎo)入\h建議10:不要在本類中覆蓋靜態(tài)導(dǎo)入的變量和方法\h建議11:養(yǎng)成良好習(xí)慣,顯式聲明UID\h建議12:避免用序列化類在構(gòu)造函數(shù)中為不變量賦值\h建議13:避免為final變量復(fù)雜賦值\h建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題\h建議15:break萬萬不可忘\h建議16:易變業(yè)務(wù)使用腳本語言編寫\h建議17:慎用動(dòng)態(tài)編譯\h建議18:避免instanceof非預(yù)期結(jié)果\h建議19:斷言絕對(duì)不是雞肋\(yùn)h建議20:不要只替換一個(gè)類\h第2章基本類型\h建議21:用偶判斷,不用奇判斷\h建議22:用整數(shù)類型處理貨幣\h建議23:不要讓類型默默轉(zhuǎn)換\h建議24:邊界,邊界,還是邊界\h建議25:不要讓四舍五入虧了一方\h建議26:提防包裝類型的null值\h建議27:謹(jǐn)慎包裝類型的大小比較\h建議28:優(yōu)先使用整型池\h建議29:優(yōu)先選擇基本類型\h建議30:不要隨便設(shè)置隨機(jī)種子\h第3章類、對(duì)象及方法\h建議31:在接口中不要存在實(shí)現(xiàn)代碼\h建議32:靜態(tài)變量一定要先聲明后賦值\h建議33:不要覆寫靜態(tài)方法\h建議34:構(gòu)造函數(shù)盡量簡化\h建議35:避免在構(gòu)造函數(shù)中初始化其他類\h建議36:使用構(gòu)造代碼塊精煉程序\h建議37:構(gòu)造代碼塊會(huì)想你所想\h建議38:使用靜態(tài)內(nèi)部類提高封裝性\h建議39:使用匿名類的構(gòu)造函數(shù)\h建議40:匿名類的構(gòu)造函數(shù)很特殊\h建議41:讓多重繼承成為現(xiàn)實(shí)\h建議42:讓工具類不可實(shí)例化\h建議43:避免對(duì)象的淺拷貝\h建議44:推薦使用序列化實(shí)現(xiàn)對(duì)象的拷貝\h建議45:覆寫equals方法時(shí)不要識(shí)別不出自己\h建議46:equals應(yīng)該考慮null值情景\h建議47:在equals中使用getClass進(jìn)行類型判斷\h建議48:覆寫equals方法必須覆寫hashCode方法\h建議49:推薦覆寫toString方法\h建議50:使用package-info類為包服務(wù)\h建議51:不要主動(dòng)進(jìn)行垃圾回收\h第4章字符串\h建議52:推薦使用String直接量賦值\h建議53:注意方法中傳遞的參數(shù)要求\h建議54:正確使用String、StringBuffer、StringBuilder\h建議55:注意字符串的位置\h建議56:自由選擇字符串拼接方法\h建議57:推薦在復(fù)雜字符串操作中使用正則表達(dá)式\h建議58:強(qiáng)烈建議使用UTF編碼\h建議59:對(duì)字符串排序持一種寬容的心態(tài)\h第5章數(shù)組和集合\h建議60:性能考慮,數(shù)組是首選\h建議61:若有必要,使用變長數(shù)組\h建議62:警惕數(shù)組的淺拷貝\h建議63:在明確的場景下,為集合指定初始容量\h建議64:多種最值算法,適時(shí)選擇\h建議65:避開基本類型數(shù)組轉(zhuǎn)換列表陷阱\h建議66:asList方法產(chǎn)生的List對(duì)象不可更改\h建議67:不同的列表選擇不同的遍歷方法\h建議68:頻繁插入和刪除時(shí)使用LinkedList\h建議69:列表相等只需關(guān)心元素?cái)?shù)據(jù)\h建議70:子列表只是原列表的一個(gè)視圖\h建議71:推薦使用subList處理局部列表\h建議72:生成子列表后不要再操作原列表\h建議73:使用Comparator進(jìn)行排序\h建議74:不推薦使用binarySearch對(duì)列表進(jìn)行檢索\h建議75:集合中的元素必須做到compareTo和equals同步\h建議76:集合運(yùn)算時(shí)使用更優(yōu)雅的方式\h建議77:使用shuffle打亂列表\h建議78:減少HashMap中元素的數(shù)量\h建議79:集合中的哈希碼不要重復(fù)\h建議80:多線程使用Vector或HashTable\h建議81:非穩(wěn)定排序推薦使用List\h建議82:由點(diǎn)及面,一葉知秋——集合大家族\h第6章枚舉和注解\h建議83:推薦使用枚舉定義常量\h建議84:使用構(gòu)造函數(shù)協(xié)助描述枚舉項(xiàng)\h建議85:小心switch帶來的空值異常\h建議86:在switch的default代碼塊中增加AssertionError錯(cuò)誤\h建議87:使用valueOf前必須進(jìn)行校驗(yàn)\h建議88:用枚舉實(shí)現(xiàn)工廠方法模式更簡潔\h建議89:枚舉項(xiàng)的數(shù)量限制在64個(gè)以內(nèi)\h建議90:小心注解繼承\(zhòng)h建議91:枚舉和注解結(jié)合使用威力更大\h建議92:注意@Override不同版本的區(qū)別\h第7章泛型和反射\h建議93:Java的泛型是類型擦除的\h建議94:不能初始化泛型參數(shù)和數(shù)組\h建議95:強(qiáng)制聲明泛型的實(shí)際類型\h建議96:不同的場景使用不同的泛型通配符\h建議97:警惕泛型是不能協(xié)變和逆變的\h建議98:建議采用的順序是List<T>、List<?>、List<Object>\h建議99:嚴(yán)格限定泛型類型采用多重界限\h建議100:數(shù)組的真實(shí)類型必須是泛型類型的子類型\h建議101:注意Class類的特殊性\h建議102:適時(shí)選擇getDeclared×××和get×××\h建議103:反射訪問屬性或方法時(shí)將Accessible設(shè)置為true\h建議104:使用forName動(dòng)態(tài)加載類文件\h建議105:動(dòng)態(tài)加載不適合數(shù)組\h建議106:動(dòng)態(tài)代理可以使代理模式更加靈活\h建議107:使用反射增加裝飾模式的普適性\h建議108:反射讓模板方法模式更強(qiáng)大\h建議109:不需要太多關(guān)注反射效率\h第8章異常\h建議110:提倡異常封裝\h建議111:采用異常鏈傳遞異常\h建議112:受檢異常盡可能轉(zhuǎn)化為非受檢異常\h建議113:不要在finally塊中處理返回值\h建議114:不要在構(gòu)造函數(shù)中拋出異常\h建議115:使用Throwable獲得棧信息\h建議116:異常只為異常服務(wù)\h建議117:多使用異常,把性能問題放一邊\h第9章多線程和并發(fā)\h建議118:不推薦覆寫start方法\h建議119:啟動(dòng)線程前stop方法是不可靠的\h建議120:不使用stop方法停止線程\h建議121:線程優(yōu)先級(jí)只使用三個(gè)等級(jí)\h建議122:使用線程異常處理器提升系統(tǒng)可靠性\h建議123:volatile不能保證數(shù)據(jù)同步\h建議124:異步運(yùn)算考慮使用Callable接口\h建議125:優(yōu)先選擇線程池\h建議126:適時(shí)選擇不同的線程池來實(shí)現(xiàn)\h建議127:Lock與synchronized是不一樣的\h建議128:預(yù)防線程死鎖\h建議129:適當(dāng)設(shè)置阻塞隊(duì)列長度\h建議130:使用CountDownLatch協(xié)調(diào)子線程\h建議131:CyclicBarrier讓多線程齊步走\(yùn)h第10章性能和效率\h建議132:提升Java性能的基本方法\h建議133:若非必要,不要克隆對(duì)象\h建議134:推薦使用“望聞問切”的方式診斷性能\h建議135:必須定義性能衡量標(biāo)準(zhǔn)\h建議136:槍打出頭鳥——解決首要系統(tǒng)性能問題\h建議137:調(diào)整JVM參數(shù)以提升性能\h建議138:性能是個(gè)大“咕咚”\h第11章開源世界\h建議139:大膽采用開源工具\(yùn)h建議140:推薦使用Guava擴(kuò)展工具包\h建議141:Apache擴(kuò)展包\h建議142:推薦使用Joda日期時(shí)間擴(kuò)展包\h建議143:可以選擇多種Collections擴(kuò)展\h第12章思想為源\h建議144:提倡良好的代碼風(fēng)格\h建議145:不要完全依靠單元測試來發(fā)現(xiàn)問題\h建議146:讓注釋正確、清晰、簡潔\h建議147:讓接口的職責(zé)保持單一\h建議148:增強(qiáng)類的可替換性\h建議149:依賴抽象而不是實(shí)現(xiàn)\h建議150:拋棄7條不良的編碼習(xí)慣\h建議151:以技術(shù)員自律而不是工人第1章Java開發(fā)中通用的方法和準(zhǔn)則Thereasonablemanadaptshimselftotheworld;theunreasonableonepersistsintryingtoadapttheworldtohimself.明白事理的人使自己適應(yīng)世界;不明事理的人想讓世界適應(yīng)自己?!挷{Java的世界豐富又多彩,但同時(shí)也布滿了荊棘陷阱,大家一不小心就可能跌入黑暗深淵,只有在了解了其通行規(guī)則后才能使自己在技術(shù)的海洋里遨游飛翔,恣意馳騁。“千里之行始于足下”,本章主要講述與Java語言基礎(chǔ)有關(guān)的問題及建議的解決方案,例如常量和變量的注意事項(xiàng)、如何更安全地序列化、斷言到底該如何使用等。建議1:不要在常量和變量中出現(xiàn)易混淆的字母包名全小寫,類名首字母全大寫,常量全部大寫并用下劃線分隔,變量采用駝峰命名法(CamelCase)命名等,這些都是最基本的Java編碼規(guī)范,是每個(gè)Javaer都應(yīng)熟知的規(guī)則,但是在變量的聲明中要注意不要引入容易混淆的字母。嘗試閱讀如下代碼,思考一下打印出的i等于多少:publicclassClient{publicstaticvoidmain(String[]args){longi=1l;System.out.println("i的兩倍是:"+(i+i));}}肯定有人會(huì)說:這么簡單的例子還能出錯(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”,只是后面加了長整型變量的標(biāo)示字母"l"而已。別說是我挖坑讓你跳,如果有類似程序出現(xiàn)在項(xiàng)目中,當(dāng)你試圖通過閱讀代碼來理解作者的思想時(shí),此情此景就有可能會(huì)出現(xiàn)。所以,為了讓您的程序更容易理解,字母"l"(還包括大寫字母"O")盡量不要和數(shù)字混用,以免使閱讀者的理解與程序意圖產(chǎn)生偏差。如果字母和數(shù)字必須混合使用,字母"l"務(wù)必大寫,字母"O"則增加注釋。注意字母"l"作為長整型標(biāo)志時(shí)務(wù)必大寫。建議2:莫讓常量蛻變成變量常量蛻變成變量?你胡扯吧,加了final和static的常量怎么可能會(huì)變呢?不可能二次賦值的呀。真的不可能嗎?看我們神奇的魔術(shù),代碼如下:publicclassClient{publicstaticvoidmain(String[]args){System.out.println("常量會(huì)變哦:"+Const.RAND_CONST);}}/*接口常量*/interfaceConst{//這還是常量嗎?publicstaticfnalintRAND_CONST=newRandom().nextInt();}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)常量使用。注意務(wù)必讓常量的值在運(yùn)行期保持不變。建議3:三元操作符的類型務(wù)必一致三元操作符是if-else的簡化寫法,在項(xiàng)目中使用它的地方很多,也非常好用,但是好用又簡單的東西并不表示就可以隨便用,我們來看看下面這段代碼:publicclassClient{publicstaticvoidmain(String[]args){inti=80;Strings=String.valueOf(i<100?90:100);Strings1=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)\h[1],則返回值類型為范圍較大者。知道是什么原因了,相應(yīng)的解決辦法也就有了:保證三元操作符中的兩個(gè)操作數(shù)類型一致,即可減少可能錯(cuò)誤的發(fā)生。\h[1]"Literal"也譯作“字面量”。建議4:避免帶有變長參數(shù)的方法重載在項(xiàng)目和系統(tǒng)的開發(fā)中,為了提高方法的靈活度和可復(fù)用性,我們經(jīng)常要傳遞不確定數(shù)量的參數(shù)到方法中,在Java5之前常用的設(shè)計(jì)技巧就是把形參定義成Collection類型或其子類類型,或者是數(shù)組類型,這種方法的缺點(diǎn)就是需要對(duì)空參數(shù)進(jìn)行判斷和篩選,比如實(shí)參為null值和長度為0的Collection或數(shù)組。而Java5引入變長參數(shù)(varags)就是為了更好地提高方法的復(fù)用性,讓方法的調(diào)用者可以“隨心所欲”地傳遞實(shí)參數(shù)量,當(dāng)然變長參數(shù)也是要遵循一定規(guī)則的,比如變長參數(shù)必須是方法中的最后一個(gè)參數(shù);一個(gè)方法不能定義多個(gè)變長參數(shù)等,這些基本規(guī)則需要牢記,但是即使記住了這些規(guī)則,仍然有可能出現(xiàn)錯(cuò)誤,我們來看如下代碼:publicclassClient{//簡單折扣計(jì)算publicvoidcalPrice(intprice,intdiscount){floatknockdownPrice=price*discount/100.0F;System.out.println("簡單折扣后的價(jià)格是:"+formateCurrency(knockdownPrice));}//復(fù)雜多折扣計(jì)算publicvoidcalPrice(intprice,int……discounts){floatknockdownPrice=price;for(intdiscount:discounts){knockdownPrice=knockdownPrice*discount/100;}System.out.println("復(fù)雜折扣后的價(jià)格是:"+formateCurrency(knockdownPrice));}//格式化成本的貨幣形式privateStringformateCurrency(floatprice){returnNumberFormat.getCurrencyInstance().format(price/100);}publicstaticvoidmain(String[]args){Clientclient=newClient();//499元的貨物,打75折client.calPrice(49900,75);}}這是一個(gè)計(jì)算商品價(jià)格折扣的模擬類,帶有兩個(gè)參數(shù)的calPrice方法(該方法的業(yè)務(wù)邏輯是:提供商品的原價(jià)和折扣率,即可獲得商品的折扣價(jià))是一個(gè)簡單的折扣計(jì)算方法,該方法在實(shí)際項(xiàng)目中經(jīng)常會(huì)用到,這是單一的打折方法。而帶有變長參數(shù)的calPrice方法則是較復(fù)雜的折扣計(jì)算方式,多種折扣的疊加運(yù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(intprice,int...discounts)的參數(shù)范疇覆蓋了calPrice(intprice,intdiscount)的參數(shù)范疇。那問題就出來了:對(duì)于calPrice(49900,75)這樣的計(jì)算,到底該調(diào)用哪個(gè)方法來處理呢?我們知道Java編譯器是很聰明的,它在編譯時(shí)會(huì)根據(jù)方法簽名(MethodSignature)來確定調(diào)用哪個(gè)方法,比如calPrice(499900,75,95)這個(gè)調(diào)用,很明顯75和95會(huì)被轉(zhuǎn)成一個(gè)包含兩個(gè)元素的數(shù)組,并傳遞到calPrice(intprice,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à)格是:¥374.25??磥硎钦{(diào)用了第一個(gè)方法,為什么會(huì)調(diào)用第一個(gè)方法,而不是第二個(gè)變長參數(shù)方法呢?因?yàn)镴ava在編譯時(shí),首先會(huì)根據(jù)實(shí)參的數(shù)量和類型(這里是2個(gè)實(shí)參,都為int類型,注意沒有轉(zhuǎn)成int數(shù)組)來進(jìn)行處理,也就是查找到calPrice(intprice,intdiscount)方法,而且確認(rèn)它是否符合方法簽名條件。現(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ì)從最簡單的開始“猜想”,只要符合編譯條件的即可通過,于是就出現(xiàn)了此問題。問題是闡述清楚了,為了讓我們的程序能被“人類”看懂,還是慎重考慮變長參數(shù)的方法重載吧,否則讓人傷腦筋不說,說不定哪天就陷入這類小陷阱里了。建議5:別讓null值和空值威脅到變長方法上一建議講解了變長參數(shù)的重載問題,本建議還會(huì)繼續(xù)討論變長參數(shù)的重載問題。上一建議的例子是變長參數(shù)的范圍覆蓋了非變長參數(shù)的范圍,這次我們從兩個(gè)都是變長參數(shù)的方法說起,代碼如下:publicclassClient{publicvoidmethodA(Stringstr,Integer……is){}publicvoidmethodA(Stringstr,String……strs){}publicstaticvoidmain(String[]args){Clientclient=newClient();client.methodA("China",0);client.methodA("China","People");client.methodA("China");client.methodA("China",null);}}兩個(gè)methodA都進(jìn)行了重載,現(xià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ù)變長參數(shù)的規(guī)范調(diào)用,傳入變長參數(shù)的實(shí)參數(shù)量可以是N個(gè)(N>=0),那當(dāng)然可以寫成client.methodA("china")方法啊!完全符合規(guī)范,但是這卻讓編譯器和調(diào)用者都很郁悶,程序符合規(guī)則卻不能運(yùn)行,如此問題,誰之責(zé)任呢?是Client類的設(shè)計(jì)者,他違反了KISS原則(KeepItSimple,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)用請求,但不知道調(diào)用哪一個(gè),于是報(bào)錯(cuò)了。我們來體會(huì)一下它的壞味道:除了不符合上面的懶人原則外,這里還有一個(gè)非常不好的編碼習(xí)慣,即調(diào)用者隱藏了實(shí)參類型,這是非常危險(xiǎn)的,不僅僅調(diào)用者需要“猜測”該調(diào)用哪個(gè)方法,而且被調(diào)用者也可能產(chǎn)生內(nèi)部邏輯混亂的情況。對(duì)于本例來說應(yīng)該做如下修改:publicstaticvoidmain(String[]args){Clientclient=newClient();String[]strs=null;client.methodA("China",strs);}也就是說讓編譯器知道這個(gè)null值是String類型的,編譯即可順利通過,也就減少了錯(cuò)誤的發(fā)生。建議6:覆寫變長方法也循規(guī)蹈矩在Java中,子類覆寫父類中的方法很常見,這樣做既可以修正Bug也可以提供擴(kuò)展的業(yè)務(wù)功能支持,同時(shí)還符合開閉原則(Open-ClosedPrinciple),我們來看一下覆寫必須滿足的條件:重寫方法不能縮小訪問權(quán)限。參數(shù)列表必須與被重寫方法相同。返回類型必須與被重寫方法的相同或是其子類。重寫方法不能拋出新的異常,或者超出父類范圍的異常,但是可以拋出更少、更有限的異常,或者不拋出異常。估計(jì)你已經(jīng)猜測出下面要講的內(nèi)容了,為什么“參數(shù)列表必須與被重寫方法的相同”采用不同的字體,這其中是不是有什么玄機(jī)?是的,還真有那么一點(diǎn)點(diǎn)小玄機(jī)。參數(shù)列表相同包括三層意思:參數(shù)數(shù)量相同、類型相同、順序相同,看上去好像沒什么問題,那我們來看一個(gè)例子,業(yè)務(wù)場景與上一個(gè)建議相同,商品打折,代碼如下:publicclassClient{publicstaticvoidmain(String[]args){//向上轉(zhuǎn)型Basebase=newSub();base.fun(100,50);//不轉(zhuǎn)型Subsub=newSub();sub.fun(100,50);}}//基類classBase{voidfun(intprice,int……discounts){System.out.println("Base……fun");}}//子類,覆寫父類方法classSubextendsBase{@Overridevoidfun(intprice,int[]discounts){System.out.println("Sub……fun");}}請問:該程序有問題嗎?——編譯通不過。那問題出在什么地方呢?@Override注解嗎?非也,覆寫是正確的,因?yàn)楦割惖腸alPrice編譯成字節(jié)碼后的形參是一個(gè)int類型的形參加上一個(gè)int數(shù)組類型的形參,子類的參數(shù)列表也與此相同,那覆寫是理所當(dāng)然的了,所以加上@Override注解沒有問題,只是Eclipse會(huì)提示這不是一種很好的編碼風(fēng)格。難道是"sub.fun(100,50)"這條語句?正解,確實(shí)是這條語句報(bào)錯(cuò),提示找不到fun(int,int)方法。這太奇怪了:子類繼承了父類的所有屬性和方法,甭管是私有的還是公開的訪問權(quán)限,同樣的參數(shù)、同樣的方法名,通過父類調(diào)用沒有任何問題,通過子類調(diào)用卻編譯通不過,為啥?難道是沒繼承下來?或者子類縮小了父類方法的前置條件?那如果是這樣,就不應(yīng)該覆寫,@Override就應(yīng)該報(bào)錯(cuò),真是奇妙的事情!事實(shí)上,base對(duì)象是把子類對(duì)象Sub做了向上轉(zhuǎn)型,形參列表是由父類決定的,由于是變長參數(shù),在編譯時(shí),"base.fun(100,50)"中的“50”這個(gè)實(shí)參會(huì)被編譯器“猜測”而編譯成“{50}”數(shù)組,再由子類Sub執(zhí)行。我們再來看看直接調(diào)用子類的情況,這時(shí)編譯器并不會(huì)把“50”做類型轉(zhuǎn)換,因?yàn)閿?shù)組本身也是一個(gè)對(duì)象,編譯器還沒有聰明到要在兩個(gè)沒有繼承關(guān)系的類之間做轉(zhuǎn)換,要知道Java是要求嚴(yán)格的類型匹配的,類型不匹配編譯器自然就會(huì)拒絕執(zhí)行,并給予錯(cuò)誤提示。這是個(gè)特例,覆寫的方法參數(shù)列表竟然與父類不相同,這違背了覆寫的定義,并且會(huì)引發(fā)莫名其妙的錯(cuò)誤。所以讀者在對(duì)變長參數(shù)進(jìn)行覆寫時(shí),如果要使用此類似的方法,請找個(gè)小黑屋仔細(xì)想想是不是一定要如此。注意覆寫的方法參數(shù)與父類相同,不僅僅是類型、數(shù)量,還包括顯示形式。建議7:警惕自增的陷阱記得大學(xué)剛開始學(xué)C語言時(shí),老師就說:自增有兩種形式,分別是i++和++i,i++表示的是先賦值后加1,++i是先加1后賦值,這樣理解了很多年也沒出現(xiàn)問題,直到遇到如下代碼,我才懷疑我的理解是不是錯(cuò)了:publicclassClient{publicstaticvoidmain(String[]args){intcount=0;for(inti=0;i<10;i++){count=count++;}System.out.println("count="+count);}}這個(gè)程序輸出的count等于幾?是count自加10次嗎?答案等于10?可以非??隙ǖ馗嬖V你,答案錯(cuò)誤!運(yùn)行結(jié)果是count等于0。為什么呢?count++是一個(gè)表達(dá)式,是有返回值的,它的返回值就是count自加前的值,Java對(duì)自加是這樣處理的:首先把count的值(注意是值,不是引用)拷貝到一個(gè)臨時(shí)變量區(qū),然后對(duì)count變量加1,最后返回臨時(shí)變量區(qū)的值。程序第一次循環(huán)時(shí)的詳細(xì)處理步驟如下:步驟1JVM把count值(其值是0)拷貝到臨時(shí)變量區(qū)。步驟2count值加1,這時(shí)候count的值是1。步驟3返回臨時(shí)變量區(qū)的值,注意這個(gè)值是0,沒修改過。步驟4返回值賦值給count,此時(shí)count值被重置成0。"count=count++"這條語句可以按照如下代碼來理解:publicstaticintmockAdd(intcount){//先保存初始值inttemp=count;//做自增操作count=count+1;//返回原始值returntemp;}于是第一次循環(huán)后count的值還是0,其他9次的循環(huán)也是一樣的,最終你會(huì)發(fā)現(xiàn)count的值始終沒有改變,仍然保持著最初的狀態(tài)。此例中代碼作者的本意是希望count自增,所以想當(dāng)然地認(rèn)為賦值給自身就成了,不曾想掉到Java自增的陷阱中了。解決方法很簡單,只要把"count=count++"修改為"count++"即可。該問題在不同的語言環(huán)境有不同的實(shí)現(xiàn):C++中"count=count++"與"count++"是等效的,而在PHP中則保持著與Java相同的處理方式。每種語言對(duì)自增的實(shí)現(xiàn)方式各不同,讀者有興趣可以多找?guī)追N語言測試一下,思考一下原理。下次如果看到某人T恤上印著"i=i++",千萬不要鄙視他,記住,能夠以不同的語言解釋清楚這句話的人絕對(duì)不簡單,應(yīng)該表現(xiàn)出“如滔滔江水”般的敬仰,心理默念著“高人,絕世高人哪”。建議8:不要讓舊語法困擾你N多年前接手了一個(gè)除了源碼以外什么都沒有的項(xiàng)目,沒需求、沒文檔、沒設(shè)計(jì),原創(chuàng)者也已鳥獸散了,我們只能通過閱讀源碼來進(jìn)行維護(hù)。期間,同事看到一段很“奇妙”的代碼,讓大家?guī)兔Ψ治觯a片段如下:publicclassClient{publicstaticvoidmain(String[]args){//數(shù)據(jù)定義及初始化intfee=200;//其他業(yè)務(wù)處理saveDefault:save(fee);//其他業(yè)務(wù)處理}staticvoidsaveDefault(){}staticvoidsave(intfee){}}該代碼的業(yè)務(wù)含義是計(jì)算交易的手續(xù)費(fèi),最低手續(xù)費(fèi)是2元,其業(yè)務(wù)邏輯大致看懂了,但是此代碼非常神奇,"saveDefault:save(fee)"這句代碼在此處出現(xiàn)后,后續(xù)就再也沒有與此有關(guān)的代碼了,這做何解釋呢?更神奇的是,編譯竟然還沒有錯(cuò),運(yùn)行也很正常。Java中竟然有冒號(hào)操作符,一般情況下,它除了在唯一一個(gè)三元操作符中存在外就沒有其他地方可用了呀。當(dāng)時(shí)連項(xiàng)目組里的高手也是一愣一愣的,翻語法書,也沒有介紹冒號(hào)操作符的內(nèi)容,而且,也不可能出現(xiàn)連括號(hào)都可以省掉的方法調(diào)用、方法級(jí)聯(lián)??!這也太牛了吧!隔壁做C項(xiàng)目的同事過來串門,看我們在討論這個(gè)問題,很驚奇地說“耶,Java中還有標(biāo)號(hào)呀,我以為Java這么高級(jí)的語言已經(jīng)拋棄goto語句了……”,一語點(diǎn)醒夢中人:項(xiàng)目的原創(chuàng)者是C語言轉(zhuǎn)過來的開發(fā)人員,所以他把C語言的goto習(xí)慣也帶到項(xiàng)目中了,后來由于經(jīng)過N手交接,重構(gòu)了多次,到我們這里goto語句已經(jīng)被重構(gòu)掉了,但是跳轉(zhuǎn)標(biāo)號(hào)還保留著,估計(jì)上一屆的重構(gòu)者也是稀里糊涂的,不敢貿(mào)然修改,所以把這個(gè)重任留給了我們。goto語句中有著"doubleface"作用的關(guān)鍵字,它可以讓程序從多層的循環(huán)中跳出,不用一層一層地退出,類似高樓著火了,來不及一樓一樓的下,goto語句就可以讓你"biu~"的一聲從十層樓跳到地面上。這點(diǎn)確實(shí)很好,但同時(shí)也帶來了代碼結(jié)構(gòu)混亂的問題,而且程序跳來跳去讓人看著就頭暈,還怎么調(diào)試?!這樣做甚至?xí)[禍連連,比如標(biāo)號(hào)前后對(duì)象構(gòu)造或變量初始化,一旦跳到這個(gè)標(biāo)號(hào),程序就不可想象了,所以Java中拋棄了goto語法,但還是保留了該關(guān)鍵字,只是不進(jìn)行語義處理而已,與此類似的還有const關(guān)鍵字。Java中雖然沒有了goto關(guān)鍵字,但是擴(kuò)展了break和continue關(guān)鍵字,它們的后面都可以加上標(biāo)號(hào)做跳轉(zhuǎn),完全實(shí)現(xiàn)了goto功能,同時(shí)也把goto的詬病帶了進(jìn)來,所以我們在閱讀大牛的開源程序時(shí),根本就看不到break或continue后跟標(biāo)號(hào)的情況,甚至是break和continue都很少看到,這是提高代碼可讀性的一劑良藥,舊語法就讓它隨風(fēng)而去吧!建議9:少用靜態(tài)導(dǎo)入從Java5開始引入了靜態(tài)導(dǎo)入語法(importstatic),其目是為了減少字符輸入量,提高代碼的可閱讀性,以便更好地理解程序。我們先來看一個(gè)不使用靜態(tài)導(dǎo)入的例子,也就是一般導(dǎo)入:publicclassMathUtils{//計(jì)算圓面積publicstaticdoublecalCircleArea(doubler){returnMath.PI*r*r;}//計(jì)算球面積publicstaticdoublecalBallArea(doubler){return4*Math.PI*r*r;}}這是很簡單的數(shù)學(xué)工具類,我們在這兩個(gè)計(jì)算面積的方法中都引入了java.lang.Math類(該類是默認(rèn)導(dǎo)入的)中的PI(圓周率)常量,而Math這個(gè)類寫在這里有點(diǎn)多余,特別是如果MathUtils中的方法比較多時(shí),如果每次都要敲入Math這個(gè)類,繁瑣且多余,靜態(tài)導(dǎo)入可解決此類問題,使用靜態(tài)導(dǎo)入后的程序如下:importstaticjava.lang.Math.PI;publicclassMathUtils{//計(jì)算圓面積publicstaticdoublecalCircleArea(doubler){returnPI*r*r;}//計(jì)算球面積publicstaticdoublecalBallArea(doubler){return4*PI*r*r;}}靜態(tài)導(dǎo)入的作用是把Math類中的PI常量引入到本類中,這會(huì)使程序更簡單,更容易閱讀,只要看到PI就知道這是圓周率,不用每次都要把類名寫全了。但是,濫用靜態(tài)導(dǎo)入會(huì)使程序更難閱讀,更難維護(hù)。靜態(tài)導(dǎo)入后,代碼中就不用再寫類名了,但是我們知道類是“一類事物的描述”,缺少了類名的修飾,靜態(tài)屬性和靜態(tài)方法的表象意義可以被無限放大,這會(huì)讓閱讀者很難弄清楚其屬性或方法代表何意,甚至是哪一個(gè)類的屬性(方法)都要思考一番(當(dāng)然,IDE友好提示功能是另說),特別是在一個(gè)類中有多個(gè)靜態(tài)導(dǎo)入語句時(shí),若還使用了*(星號(hào))通配符,把一個(gè)類的所有靜態(tài)元素都導(dǎo)入進(jìn)來了,那簡直就是惡夢。我們來看一段例子:importstaticjava.lang.Double.*;importstaticjava.lang.Math.*;importstaticjava.lang.Integer.*;importstaticjava.text.NumberFormat.*;publicclassClient{//輸入半徑和精度要求,計(jì)算面積publicstaticvoidmain(String[]args){doubles=PI*parseDouble(args[0]);NumberFormatnf=getInstance();nf.setMaximumFractionDigits(parseInt(args[1]));formatMessage(nf.format(s));}//格式化消息輸出publicstaticvoidformatMessage(Strings){System.out.println("圓面積是:"+s);}}就這么一段程序,看著就讓人火大:常量PI,這知道,是圓周率;parseDouble方法可能是Double類的一個(gè)轉(zhuǎn)換方法,這看名稱也能猜測到。那緊接著的getInstance方法是哪個(gè)類的?是Client本地類?不對(duì)呀,沒有這個(gè)方法,哦,原來是NumberFormate類的方法,這和formateMessage本地方法沒有任何區(qū)別了——這代碼也太難閱讀了,非機(jī)器不可閱讀。所以,對(duì)于靜態(tài)導(dǎo)入,一定要遵循兩個(gè)規(guī)則:不使用*(星號(hào))通配符,除非是導(dǎo)入靜態(tài)常量類(只包含常量的類或接口)。方法名是具有明確、清晰表象意義的工具類。何為具有明確、清晰表象意義的工具類?我們來看看JUnit4中使用的靜態(tài)導(dǎo)入的例子,代碼如下:importstaticorg.junit.Assert.*;publicclassDaoTest{@TestpublicvoidtestInsert(){//斷言assertEquals("foo","foo");assertFalse(Boolean.FALSE);}}我們從程序中很容易判斷出assertEquals方法是用來斷言兩個(gè)值是否相等的,assertFalse方法則是斷言表達(dá)式為假,如此確實(shí)減少了代碼量,而且代碼的可讀性也提高了,這也是靜態(tài)導(dǎo)入用到正確地方所帶來的好處。建議10:不要在本類中覆蓋靜態(tài)導(dǎo)入的變量和方法如果一個(gè)類中的方法及屬性與靜態(tài)導(dǎo)入的方法及屬性重名會(huì)出現(xiàn)什么問題呢?我們先來看一個(gè)正常的靜態(tài)導(dǎo)入,代碼如下:importstaticjava.lang.Math.PI;importstaticjava.lang.Math.abs;publicclassClient{publicstaticvoidmain(String[]args){System.out.println("PI="+PI);System.out.println("abs(100)="+abs(-100));}}很簡單的例子,打印出靜態(tài)常量PI值,計(jì)算-100的絕對(duì)值?,F(xiàn)在的問題是:如果我們在Client類中也定義了PI常量和abs方法,會(huì)出現(xiàn)什么問題?代碼如下:importstaticjava.lang.Math.PI;importstaticjava.lang.Math.abs;publicclassClient{//常量名與靜態(tài)導(dǎo)入的PI相同publicfnalstaticStringPI="祖沖之";//方法名與靜態(tài)導(dǎo)入的相同publicstaticintabs(intabs){return0;}publicstaticvoidmain(String[]args){System.out.println("PI="+PI);System.out.println("abs(100)="+abs(-100));}}以上代碼中,定義了一個(gè)PI字符串類型的常量,又定義了一個(gè)abs方法,與靜態(tài)導(dǎo)入的相同。首先說好消息:編譯器沒有報(bào)錯(cuò),接下來是不好的消息了:我們不知道哪個(gè)屬性和哪個(gè)方法被調(diào)用了,因?yàn)槌A棵头椒嗤?,到底調(diào)用了哪一個(gè)方法呢?我們運(yùn)行一下看看結(jié)果:PI=祖沖之a(chǎn)bs(100)=0很明顯是本地的屬性和方法被引用了,為什么不是Math類中的屬性和方法呢?那是因?yàn)榫幾g器有一個(gè)“最短路徑”原則:如果能夠在本類中查找到的變量、常量、方法,就不會(huì)到其他包或父類、接口中查找,以確保本類中的屬性、方法優(yōu)先。因此,如果要變更一個(gè)被靜態(tài)導(dǎo)入的方法,最好的辦法是在原始類中重構(gòu),而不是在本類中覆蓋。建議11:養(yǎng)成良好習(xí)慣,顯式聲明UID我們編寫一個(gè)實(shí)現(xiàn)了Serializable接口(序列化標(biāo)志接口)的類,Eclipse馬上就會(huì)給一個(gè)黃色警告:需要增加一個(gè)SerialVersionID。為什么要增加?它是怎么計(jì)算出來的?有什么用?本章就來解釋該問題。類實(shí)現(xiàn)Serializable接口的目的是為了可持久化,比如網(wǎng)絡(luò)傳輸或本地存儲(chǔ),為系統(tǒng)的分布和異構(gòu)部署提供先決支持條件。若沒有序列化,現(xiàn)在我們熟悉的遠(yuǎn)程調(diào)用、對(duì)象數(shù)據(jù)庫都不可能存在,我們來看一個(gè)簡單的序列化類:publicclassPersonimplementsSerializable{privateStringname;/*name屬性的getter/setter方法省略*/}這是一個(gè)簡單JavaBean,實(shí)現(xiàn)了Serializable接口,可以在網(wǎng)絡(luò)上傳輸,也可以本地存儲(chǔ)然后讀取。這里我們以Java消息服務(wù)(JavaMessageService)方式傳遞該對(duì)象(即通過網(wǎng)絡(luò)傳遞一個(gè)對(duì)象),定義在消息隊(duì)列中的數(shù)據(jù)類型為ObjectMessage,首先定義一個(gè)消息的生產(chǎn)者(Producer),代碼如下:publicclassProducer{publicstaticvoidmain(String[]args)throwsException{Personperson=newPerson();person.setName("混世魔王");//序列化,保存到磁盤上SerializationUtils.writeObject(person);}}這里引入了一個(gè)工具類SerializationUtils,其作用是對(duì)一個(gè)類進(jìn)行序列化和反序列化,并存儲(chǔ)到硬盤上(模擬網(wǎng)絡(luò)傳輸),其代碼如下:publicclassSerializationUtils{privatestaticStringFILE_NAME="c:/obj.bin";//序列化publicstaticvoidwriteObject(Serializables){try{ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream(FILE_NAME));oos.writeObject(s);oos.close();}catch(Exceptione){e.printStackTrace();}}publicstaticObjectreadObject(){Objectobj=null;//反序列化try{ObjectInputinput=newObjectInputStream(newFileInputStream(FILE_NAME));obj=input.readObject();input.close();}catch(Exceptione){e.printStackTrace();}returnobj;}}通過對(duì)象序列化過程,把一個(gè)對(duì)象從內(nèi)存塊轉(zhuǎn)化為可傳輸?shù)臄?shù)據(jù)流,然后通過網(wǎng)絡(luò)發(fā)送到消息消費(fèi)者(Consumer)那里,并進(jìn)行反序列化,生成實(shí)例對(duì)象,代碼如下:publicclassConsumer{publicstaticvoidmain(String[]args)throwsException{//反序列化Personp=(Person)SerializationUtils.readObject();System.out.println("name="+p.getName());}}這是一個(gè)反序列化過程,也就是對(duì)象數(shù)據(jù)流轉(zhuǎn)換為一個(gè)實(shí)例對(duì)象的過程,其運(yùn)行后的輸出結(jié)果為:混世魔王。這太easy了,是的,這就是序列化和反序列化典型的demo。但此處隱藏著一個(gè)問題:如果消息的生產(chǎn)者和消息的消費(fèi)者所參考的類(Person類)有差異,會(huì)出現(xiàn)何種神奇事件?比如:消息生產(chǎn)者中的Person類增加了一個(gè)年齡屬性,而消費(fèi)者沒有增加該屬性。為啥沒有增加?!因?yàn)檫@是個(gè)分布式部署的應(yīng)用,你甚至都不知道這個(gè)應(yīng)用部署在何處,特別是通過廣播(broadcast)方式發(fā)送消息的情況,漏掉一兩個(gè)訂閱者也是很正常的。在這種序列化和反序列化的類不一致的情形下,反序列化時(shí)會(huì)報(bào)一個(gè)InvalidClassException異常,原因是序列化和反序列化所對(duì)應(yīng)的類版本發(fā)生了變化,JVM不能把數(shù)據(jù)流轉(zhuǎn)換為實(shí)例對(duì)象。接著刨根問底:JVM是根據(jù)什么來判斷一個(gè)類版本的呢?好問題,通過SerialVersionUID,也叫做流標(biāo)識(shí)符(StreamUniqueIdentifier),即類的版本定義的,它可以顯式聲明也可以隱式聲明。顯式聲明格式如下:privatestaticfinallongserialVersionUID=XXXXXL;而隱式聲明則是我不聲明,你編譯器在編譯的時(shí)候幫我生成。生成的依據(jù)是通過包名、類名、繼承關(guān)系、非私有的方法和屬性,以及參數(shù)、返回值等諸多因子計(jì)算得出的,極度復(fù)雜,基本上計(jì)算出來的這個(gè)值是唯一的。serialVersionUID如何生成已經(jīng)說明了,我們再來看看serialVersionUID的作用。JVM在反序列化時(shí),會(huì)比較數(shù)據(jù)流中的serialVersionUID與類的serialVersionUID是否相同,如果相同,則認(rèn)為類沒有發(fā)生改變,可以把數(shù)據(jù)流load為實(shí)例對(duì)象;如果不相同,對(duì)不起,我JVM不干了,拋個(gè)異常InvalidClassException給你瞧瞧。這是一個(gè)非常好的校驗(yàn)機(jī)制,可以保證一個(gè)對(duì)象即使在網(wǎng)絡(luò)或磁盤中“滾過”一次,仍能做到“出淤泥而不染”,完美地實(shí)現(xiàn)類的一致性。但是,有時(shí)候我們需要一點(diǎn)特例場景,例如:我的類改變不大,JVM是否可以把我以前的對(duì)象反序列化過來?就是依靠顯式聲明serialVersionUID,向JVM撒謊說“我的類版本沒有變更”,如此,我們編寫的類就實(shí)現(xiàn)了向上兼容。我們修改一下上面的Person類,代碼如下:publicclassPersonimplementsSerializable{privatestaticfnallongserialVersionUID=55799L;/*其他保持不變*/}剛開始生產(chǎn)者和消費(fèi)者持有的Person類版本一致,都是V1.0,某天生產(chǎn)者的Person類版本變更了,增加了一個(gè)“年齡”屬性,升級(jí)為V2.0,而由于種種原因(比如程序員疏忽、升級(jí)時(shí)間窗口不同等)消費(fèi)端的Person還保持為V1.0版本,代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=5799L;privateintage;/*age、name的getter/setter方法省略*/}此時(shí)雖然生產(chǎn)者和消費(fèi)者對(duì)應(yīng)的類版本不同,但是顯式聲明的serialVersionUID相同,反序列化也是可以運(yùn)行的,所帶來的業(yè)務(wù)問題就是消費(fèi)端不能讀取到新增的業(yè)務(wù)屬性(age屬性)而已。通過此例,我們的反序列化實(shí)現(xiàn)了版本向上兼容的功能,使用V1.0版本的應(yīng)用訪問了一個(gè)V2.0版本的對(duì)象,這無疑提高了代碼的健壯性。我們在編寫序列化類代碼時(shí),隨手加上serialVersionUID字段,也不會(huì)給我們帶來太多的工作量,但它卻可以在關(guān)鍵時(shí)候發(fā)揮異乎尋常的作用。注意顯式聲明serialVersionUID可以避免對(duì)象不一致,但盡量不要以這種方式向JVM“撒謊”。建議12:避免用序列化類在構(gòu)造函數(shù)中為不變量賦值我們知道帶有final標(biāo)識(shí)的屬性是不變量,也就是說只能賦值一次,不能重復(fù)賦值,但是在序列化類中就有點(diǎn)復(fù)雜了,比如有這樣一個(gè)類:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=71282334L;//不變量publicfnalStringname="混世魔王";}這個(gè)Person類(此時(shí)V1.0版本)被序列化,然后存儲(chǔ)在磁盤上,在反序列化時(shí)name屬性會(huì)重新計(jì)算其值(這與static變量不同,static變量壓根就沒有保存到數(shù)據(jù)流中),比如name屬性修改成了“德天使”(版本升級(jí)為V2.0),那么反序列化對(duì)象的name值就是“德天使”。保持新舊對(duì)象的final變量相同,有利于代碼業(yè)務(wù)邏輯統(tǒng)一,這是序列化的基本規(guī)則之一,也就是說,如果final屬性是一個(gè)直接量,在反序列化時(shí)就會(huì)重新計(jì)算。對(duì)這基本規(guī)則不多說,我們要說的是final變量另外一種賦值方式:通過構(gòu)造函數(shù)賦值。代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=91282334L;//不變量初始不賦值publicfinalStringname;//構(gòu)造函數(shù)為不變量賦值publicPerson(){name="混世魔王";}}這也是我們常用的一種賦值方式,可以把這個(gè)Person類定義為版本V1.0,然后進(jìn)行序列化,看看有什么問題沒有,序列化的代碼如下所示:publicclassSerialize{publicstaticvoidmain(String[]args){//序列化以持久保存SerializationUtils.writeObject(newPerson());}}Person的實(shí)例對(duì)象保存到了磁盤上,它是一個(gè)貧血對(duì)象(承載業(yè)務(wù)屬性定義,但不包含其行為定義),我們做一個(gè)簡單的模擬,修改一下name值代表變更,要注意的是serialVersionUID保持不變,修改后的代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=91282334L;//不變量初始不賦值publicfinalStringname;//構(gòu)造函數(shù)為不變量賦值publicPerson(){name="德天使";}}此時(shí)Person類的版本是V2.0,但serialVersionUID沒有改變,仍然可以反序列化,其代碼如下:publicclassDeserialize{publicstaticvoidmain(String[]args){//反序列化Personp=(Person)SerializationUtils.readObject();System.out.println();}}現(xiàn)在問題來了:打印的結(jié)果是什么?是混世魔王還是德天使?答案即將揭曉,答案是:混世魔王。final類型的變量不是會(huì)重新計(jì)算嗎?答案應(yīng)該是“德天使”才對(duì)啊,為什么會(huì)是“混世魔王”?這是因?yàn)檫@里觸及了反序列化的另一個(gè)規(guī)則:反序列化時(shí)構(gòu)造函數(shù)不會(huì)執(zhí)行。反序列化的執(zhí)行過程是這樣的:JVM從數(shù)據(jù)流中獲取一個(gè)Object對(duì)象,然后根據(jù)數(shù)據(jù)流中的類文件描述信息(在序列化時(shí),保存到磁盤的對(duì)象文件中包含了類描述信息,注意是類描述信息,不是類)查看,發(fā)現(xiàn)是final變量,需要重新計(jì)算,于是引用Person類中的name值,而此時(shí)JVM又發(fā)現(xiàn)name竟然沒有賦值,不能引用,于是它很“聰明”地不再初始化,保持原值狀態(tài),所以結(jié)果就是“混世魔王”了。讀者不要以為這樣的情況很少發(fā)生,如果使用Java開發(fā)過桌面應(yīng)用,特別是參與過對(duì)性能要求較高的項(xiàng)目(比如交易類項(xiàng)目),那么很容易遇到這樣的問題。比如一個(gè)C/S結(jié)構(gòu)的在線外匯交易系統(tǒng),要求提供24小時(shí)的聯(lián)機(jī)服務(wù),如果在升級(jí)的類中有一個(gè)final變量是構(gòu)造函數(shù)賦值的,而且新舊版本還發(fā)生了變化,則在應(yīng)用請求熱切的過程中(非常短暫,可能只有30秒),很可能就會(huì)出現(xiàn)反序列化生成的final變量值與新產(chǎn)生的實(shí)例值不相同的情況,于是業(yè)務(wù)異常就產(chǎn)生了,情況嚴(yán)重的話甚至?xí)绊懡灰讛?shù)據(jù),那可是天大的事故了。注意在序列化類中,不使用構(gòu)造函數(shù)為final變量賦值。建議13:避免為final變量復(fù)雜賦值為final變量賦值還有一種方式:通過方法賦值,即直接在聲明時(shí)通過方法返回值賦值。還是以Person類為例來說明,代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=91282334L;//通過方法返回值為final變量賦值publicfnalStringname=initName();//初始化方法名publicStringinitName(){return"混世魔王";}}name屬性是通過initName方法的返回值賦值的,這在復(fù)雜類中經(jīng)常用到,這比使用構(gòu)造函數(shù)賦值更簡潔、易修改,那么如此用法在序列化時(shí)會(huì)不會(huì)有問題呢?我們一起來看看。Person類寫好了(定義為V1.0版本),先把它序列化,存儲(chǔ)到本地文件,其代碼與上一建議的Serialize類相同,不再贅述?,F(xiàn)在,Person類的代碼需要修改,initName的返回值也改變了,代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=91282334L;//通過方法返回值為final變量賦值publicfinalStringname=initName();//初始化方法名publicStringinitName(){return"德天使";}}上段代碼僅僅修改了initName的返回值(Person類為V2.0版本),也就是說通過new生成的Person對(duì)象的final變量值都是“德天使”。那么我們把之前存儲(chǔ)在磁盤上的實(shí)例加載上來,name值會(huì)是什么呢?結(jié)果是:混世魔王。很詫異,上一建議說過final變量會(huì)被重新賦值,但是這個(gè)例子又沒有重新賦值,為什么?上個(gè)建議所說final會(huì)被重新賦值,其中的“值”指的是簡單對(duì)象。簡單對(duì)象包括:8個(gè)基本類型,以及數(shù)組、字符串(字符串情況很復(fù)雜,不通過new關(guān)鍵字生成String對(duì)象的情況下,final變量的賦值與基本類型相同),但是不能方法賦值。其中的原理是這樣的,保存到磁盤上(或網(wǎng)絡(luò)傳輸)的對(duì)象文件包括兩部分:(1)類描述信息包括包路徑、繼承關(guān)系、訪問權(quán)限、變量描述、變量訪問權(quán)限、方法簽名、返回值,以及變量的關(guān)聯(lián)類信息。要注意的一點(diǎn)是,它并不是class文件的翻版,它不記錄方法、構(gòu)造函數(shù)、static變量等的具體實(shí)現(xiàn)。之所以類描述會(huì)被保存,很簡單,是因?yàn)槟苋ヒ材芑芈?,這保證反序列化的健壯運(yùn)行。(2)非瞬態(tài)(transient關(guān)鍵字)和非靜態(tài)(static關(guān)鍵字)的實(shí)例變量值注意,這里的值如果是一個(gè)基本類型,好說,就是一個(gè)簡單值保存下來;如果是復(fù)雜對(duì)象,也簡單,連該對(duì)象和關(guān)聯(lián)類信息一起保存,并且持續(xù)遞歸下去(關(guān)聯(lián)類也必須實(shí)現(xiàn)Serializable接口,否則會(huì)出現(xiàn)序列化異常),也就是說遞歸到最后,其實(shí)還是基本數(shù)據(jù)類型的保存。正是因?yàn)檫@兩點(diǎn)原因,一個(gè)持久化后的對(duì)象文件會(huì)比一個(gè)class類文件大很多,有興趣的讀者可以自己寫個(gè)Helloword程序檢驗(yàn)一下,其體積確實(shí)膨脹了不少。總結(jié)一下,反序列化時(shí)final變量在以下情況下不會(huì)被重新賦值:通過構(gòu)造函數(shù)為final變量賦值。通過方法返回值為final變量賦值。final修飾的屬性不是基本類型。建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題部分屬性持久化問題看似很簡單,只要把不需要持久化的屬性加上瞬態(tài)關(guān)鍵字(transient關(guān)鍵字)即可。這是一種解決方案,但有時(shí)候行不通。例如一個(gè)計(jì)稅系統(tǒng)和人力資源系統(tǒng)(HR系統(tǒng))通過RMI(RemoteMethodInvocation,遠(yuǎn)程方法調(diào)用)對(duì)接,計(jì)稅系統(tǒng)需要從HR系統(tǒng)獲得人員的姓名和基本工資,以作為納稅的依據(jù),而HR系統(tǒng)的工資分為兩部分:基本工資和績效工資,基本工資沒什么秘密,根據(jù)工作崗位和年限自己都可以計(jì)算出來,但績效工資卻是保密的,不能泄露到外系統(tǒng),很明顯這是兩個(gè)相互關(guān)聯(lián)的類。先來看薪水類Salary類的代碼:publicclassSalaryimplementsSerializable{privatestaticfinallongserialVersionUID=44663L;//基本工資privateintbasePay;//績效工資privateintbonus;publicSalary(int_basePay,int_bonus){basePay=_basePay;bonus=_bonus;}/*getter/setter方法省略*/}Peron類與Salary類是關(guān)聯(lián)關(guān)系,代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=60407L;//姓名privateStringname;//薪水privateSalarysalary;publicPerson(String_name,Salary_salary){name=_name;salary=_salary;}/*getter/setter方法省略*/}這是兩個(gè)簡單的JavaBean,都實(shí)現(xiàn)了Serializable接口,都具備了持久化條件。首先計(jì)稅系統(tǒng)請求HR系統(tǒng)對(duì)某一個(gè)Person對(duì)象進(jìn)行序列化,把人員和工資信息傳遞到計(jì)稅系統(tǒng)中,代碼如下:publicclassSerialize{publicstaticvoidmain(String[]args){//基本工資1000元,績效工資2500元Salarysalary=newSalary(1000,2500);//記錄人員信息Personperson=newPerson("張三",salary);//HR系統(tǒng)持久化,并傳遞到計(jì)稅系統(tǒng)SerializationUtils.writeObject(person);}}在通過網(wǎng)絡(luò)傳送到計(jì)稅系統(tǒng)后,進(jìn)行反序列化,代碼如下:publicclassDeserialize{publicstaticvoidmain(String[]args){//技術(shù)系統(tǒng)反序列化,并打印信息Personp=(Person)SerializationUtils.readObject();StringBuffersb=newStringBuffer();sb.append("姓名:"+p.getName());sb.append("\t基本工資:"+p.getSalary().getBasePay());sb.append("\t績效工資:"+p.getSalary().getBonus());System.out.println(sb);}}打印出的結(jié)果很簡單:姓名:張三基本工資:1000績效工資:2500。但是這不符合需求,因?yàn)橛?jì)稅系統(tǒng)只能從HR系統(tǒng)中獲得人員姓名和基本工資,而績效工資是不能獲得的,這是個(gè)保密數(shù)據(jù),不允許發(fā)生泄露。怎么解決這個(gè)問題呢?你可能馬上會(huì)想到四種方案:(1)在bonus前加上transient關(guān)鍵字這是一個(gè)方法,但不是一個(gè)好方法,加上transient關(guān)鍵字就標(biāo)志著Salary類失去了分布式部署的功能,它可是HR系統(tǒng)最核心的類了,一旦遭遇性能瓶頸,想再實(shí)現(xiàn)分布式部署就不可能了,此方案否定。(2)新增業(yè)務(wù)對(duì)象增加一個(gè)Person4Tax類,完全為計(jì)稅系統(tǒng)服務(wù),就是說它只有兩個(gè)屬性:姓名和基本工資。符合開閉原則,而且對(duì)原系統(tǒng)也沒有侵入性,只是增加了工作量而已。這是個(gè)方法,但不是最優(yōu)方法。(3)請求端過濾在計(jì)稅系統(tǒng)獲得Person對(duì)象后,過濾掉Salary的bonus屬性,方案可行但不合規(guī)矩,因?yàn)镠R系統(tǒng)中的Salary類安全性竟然讓外系統(tǒng)(計(jì)稅系統(tǒng))來承擔(dān),設(shè)計(jì)嚴(yán)重失職。(4)變更傳輸契約例如改用XML傳輸,或者重建一個(gè)WebService服務(wù)??梢宰?,但成本太高??赡苡凶x者會(huì)說了,你都在說別人的方案不好,你提供個(gè)優(yōu)秀的方案看看!好的,這就展示一個(gè)優(yōu)秀的方案。其中,實(shí)現(xiàn)了Serializable接口的類可以實(shí)現(xiàn)兩個(gè)私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。我們把Person類稍做修改,看看如何控制序列化和反序列化,代碼如下:publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=60407L;//姓名privateStringname;//薪水privatetransientSalarysalary;publicPerson(String_name,Salary_salary){name=_name;salary=_salary;}//序列化委托方法privatevoidwriteObject(java.io.ObjectOutputStreamout)throwsIOException{out.defaultWriteObject();out.writeInt(salary.getBasePay());}//反序列化時(shí)委托方法privatevoidreadObject(java.io.ObjectInputStreamin)throwsIOException,Class-NotFoundException{in.defaultReadObject();salary=newSalary(in.readInt(),0);}}其他代碼不做任何改動(dòng),我們先運(yùn)行看看,結(jié)果為:姓名:張三基本工資:1000績效工資:0。我們在Person類中增加了writeObject和readObject兩個(gè)方法,并且訪問權(quán)限都是私有級(jí)別,為什么這會(huì)改變程序的運(yùn)行結(jié)果呢?其實(shí)這里使用了序列化獨(dú)有的機(jī)制:序列化回調(diào)。Java調(diào)用ObjectOutputStream類把一個(gè)對(duì)象轉(zhuǎn)換成流數(shù)據(jù)時(shí),會(huì)通過反射(Reflection)檢查被序列化的類是否有writeObject方法,并且檢查其是否符合私有、無返回值的特性。若有,則會(huì)委托該方法進(jìn)行對(duì)象序列化,若沒有,則由ObjectOutputStream按照默認(rèn)規(guī)則繼續(xù)序列化。同樣,在從流數(shù)據(jù)恢復(fù)成實(shí)例對(duì)象時(shí),也會(huì)檢查是否有一個(gè)私有的readObject方法,如果有,則會(huì)通過該方法讀取屬性值。此處有幾個(gè)關(guān)鍵點(diǎn)要說明:(1)out.defaultWriteObject()告知JVM按照默認(rèn)的規(guī)則寫入對(duì)象,慣例的寫法是寫在第一句話里。(2)in.defaultReadObject()告知JVM按照默認(rèn)規(guī)則讀入對(duì)象,慣例的寫法也是寫在第一句話里。(3)out.writeXX和in.readXX分別是寫入和讀出相應(yīng)的值,類似一個(gè)隊(duì)列,先進(jìn)先出,如果此處有復(fù)雜的數(shù)據(jù)邏輯,建議按封裝Collection對(duì)象處理??赡苡凶x者會(huì)提出,這似乎不是一種優(yōu)雅的處理方案呀,為什么JDK沒有對(duì)此提供一個(gè)更好的解決辦法呢?比如訪問者模式,或者設(shè)置鉤子函數(shù)(Hook),完全可以更優(yōu)雅地解決此類問題。我查閱了大量的文檔,得出的結(jié)論是:無解,只能說這是一個(gè)可行的解決方案而已。再回到我們的業(yè)務(wù)領(lǐng)域,通過上述方法重構(gòu)后,其代碼的修改量減少了許多,也優(yōu)雅了許多??赡苣阌忠磫柫耍喝绱艘粊?,Person類也失去了分布式部署的能力啊。確實(shí)是,但是HR系統(tǒng)的難點(diǎn)和重點(diǎn)是薪水計(jì)算,特別是績效工資,它所依賴的參數(shù)很復(fù)雜(僅從數(shù)量上說就有上百甚至上千種),計(jì)算公式也不簡單(一般是引入腳本語言,個(gè)性化公式定制),而相對(duì)來說Person類基本上都是“靜態(tài)”屬性,計(jì)算的可能性不大,所以即使為性能考慮,Person類為分布式部署的意義也不大。建議15:break萬萬不可忘我們經(jīng)常會(huì)寫一些轉(zhuǎn)換類,比如貨幣轉(zhuǎn)換、日期轉(zhuǎn)換、編碼轉(zhuǎn)換等,在金融領(lǐng)域里用到最多的要數(shù)中文數(shù)字轉(zhuǎn)換了,比如把“1”轉(zhuǎn)換為“壹”,不過,開源世界是不會(huì)提供此工具類的,因?yàn)樗N合中國文化了,要轉(zhuǎn)換還是得自己動(dòng)手寫,代碼片段如下:publicclassClient{publicstaticvoidmain(String[]args){System.out.println("2="+toChineseNumberCase(2));}//把阿拉伯?dāng)?shù)字翻譯成中文大寫數(shù)字publicstaticStringtoChineseNumberCase(intn){StringchineseNumber="";switch(n){case0:chineseNumber="零";case1:chineseNumber="壹";case2:chineseNumber="貳";case3:chineseNumber="叁";case4:chineseNumber="肆";case5:chineseNumber="伍";case6:chineseNumber="陸";case7:chineseNumber="柒";case8:chineseNumber="捌";case9:chineseN
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 銀監(jiān)會(huì) 抵押合同范本
- 科技類家居產(chǎn)品的創(chuàng)意廣告與市場接受度分析
- 科技引領(lǐng)下的綠色辦公環(huán)境建設(shè)
- 現(xiàn)代商業(yè)綜合體中的別墅空間規(guī)劃研究
- 知識(shí)產(chǎn)教育在醫(yī)療設(shè)備研發(fā)中的實(shí)踐應(yīng)用
- 2025年邯鄲科技職業(yè)學(xué)院單招職業(yè)傾向性測試題庫完整
- 集成房屋定制合同范本
- 集體荒山招租合同范本
- 2025-2030年中國甘氨酸市場風(fēng)險(xiǎn)評(píng)估及發(fā)展規(guī)模分析報(bào)告
- 2025-2030年中國玉米蛋白粉市場運(yùn)營狀況及發(fā)展前景分析報(bào)告
- 三方公司合作協(xié)議書范本
- 護(hù)理責(zé)任組長續(xù)聘競聘
- 2024-2025學(xué)年第二學(xué)期教學(xué)教研工作安排表
- 2025年貴州云上產(chǎn)業(yè)服務(wù)有限公司招聘筆試參考題庫含答案解析
- 2025年南京信息職業(yè)技術(shù)學(xué)院高職單招職業(yè)適應(yīng)性測試近5年??及鎱⒖碱}庫含答案解析
- 2025-2030年中國天然氣行業(yè)發(fā)展分析及發(fā)展趨勢預(yù)測報(bào)告
- 《雷達(dá)信號(hào)處理基礎(chǔ)》課件
- 2025屆貴州省興義市三年級(jí)數(shù)學(xué)第一學(xué)期期末達(dá)標(biāo)檢測試題含解析
- 人教版地理七年級(jí)下冊7.1.2 亞洲的自然環(huán)境(課件39張)
- 外研版(三起)小學(xué)英語三年級(jí)下冊Unit 1 Animal friends Get ready start up 課件
- 2025年交通運(yùn)輸部廣州打撈局招聘事業(yè)編制人員13人歷年管理單位筆試遴選500模擬題附帶答案詳解
評(píng)論
0/150
提交評(píng)論