第一章 Java開發(fā)中通用的方法和準(zhǔn)則 建議1:不要在常量和變量中出現(xiàn)易混淆的字母; (i、l、1;o、0等)。 建議2:莫讓常量蛻變成變量; (代碼運行工程中不要改變常量值)。 建議3:三元操作符的類型務(wù)必一致; (不一致會導(dǎo)致自動類型轉(zhuǎn)換,類型提升int->float->double等)。 建議4:避免帶有變長參數(shù)的方法重載; (變長參數(shù)的方法重載之后可能會包含原方法)。 建議5:別讓null值和空值威脅到變長方法; (兩個都包含變長參數(shù)的重載方法,當(dāng)變長參數(shù)部分空值,或者為null值時,重載方法不清楚會調(diào)用哪一個方法)。 建議6:覆寫變長方法也循規(guī)蹈矩; (變長參數(shù)與數(shù)組,覆寫的方法參數(shù)與父類相同,不僅僅是類型、數(shù)量,還包括顯示形式)。 建議7:警惕自增的陷阱; (count=count++;操作時JVM首先將count原值拷貝到臨時變量區(qū),再執(zhí)行count加1,之后再將臨時變量區(qū)的值賦給count,所以count一直為0或者某個初始值。C++中count=count++;與count++等效,而PHP與Java類似)。 建議8:不要讓舊語法困擾你; (Java中拋棄了C語言中的goto語法,但是還保留了該關(guān)鍵字,只是不進(jìn)行語義處理,const關(guān)鍵中同樣類似)。 建議9:少用靜態(tài)導(dǎo)入; (Java5引入的靜態(tài)導(dǎo)入語法import static,使用靜態(tài)導(dǎo)入可以減少程序字符輸入量,但是會帶來很多代碼歧義,省略的類約束太少,顯得程序晦澀難懂)。 建議10:不要在本類中覆蓋靜態(tài)導(dǎo)入的變量和方法; (例如靜態(tài)導(dǎo)入Math包下的PI常量,類屬性中又定義了一個同樣名字PI的常量。編譯器的“最短路徑原則”將會選擇使用本類中的PI常量。本類中的屬性,方法優(yōu)先。如果要變更一個被靜態(tài)導(dǎo)入的方法,最好的辦法是在原始類中重構(gòu),而不是在本類中覆蓋)。 建議11:養(yǎng)成良好的習(xí)慣,顯式聲明UID; (顯式聲明serialVersionUID可以避免序列化和反序列化中對象不一致,JVM根據(jù)serialVersionUID來判斷類是否發(fā)生改變。隱式聲明由編譯器在編譯的時候根據(jù)包名、類名、繼承關(guān)系等諸多因子計算得出,極其復(fù)雜,算出的值基本唯一)。 建議12:避免用序列化類在構(gòu)造函數(shù)中為不變量賦值; (在序列化類中,不適用構(gòu)造函數(shù)為final變量賦值)(序列化規(guī)則1:如果final屬性是一個直接量,在反序列化時就會重新計算;序列化規(guī)則2:反序列化時構(gòu)造函數(shù)不會執(zhí)行;反序列化執(zhí)行過程:JVM從數(shù)據(jù)流中獲取一個Object對象,然后根據(jù)數(shù)據(jù)流中的類文件描述信息(在序列化時,保存到磁盤的對象文件中包含了類描述信息,不是類)查看,發(fā)現(xiàn)是final變量,需要重新計算,于是引用Person類中的name值,而此時JVM又發(fā)現(xiàn)name沒有賦值(因為反序列化時構(gòu)造函數(shù)不會執(zhí)行),不能引用,于是它不再初始化,保持原始值狀態(tài)。整個過程中需要保持serialVersionUID相同)。 建議13:避免為final變量復(fù)雜賦值; (類序列化保存到磁盤上(或網(wǎng)絡(luò)傳輸)的對象文件包括兩部分:1、類描述信息:包括包路徑、繼承關(guān)系等。注意,它并不是class文件的翻版,不記錄方法、構(gòu)造函數(shù)、static變量等的具體實現(xiàn)。2、非瞬態(tài)(transient關(guān)鍵字)和非靜態(tài)(static關(guān)鍵字)的實例變量值??偨Y(jié):反序列化時final變量在以下情況下不會被重新賦值:1、通過構(gòu)造函數(shù)為final變量賦值;2、通過方法返回值為final變量賦值;3、final修飾的屬性不是基本類型)。 建議14:使用序列化類的私有方法巧妙解決“部分屬性持久化問題”; (部分屬性持久化問題解決方案1:把不需要持久化的屬性加上瞬態(tài)關(guān)鍵字(transient關(guān)鍵字)即可,但是會使該類失去了分布式部署的功能。方案2:新增業(yè)務(wù)對象。方案3:請求端過濾。方案4:變更傳輸契約,即覆寫writeObject和readObject私有方法,在兩個私有方法體內(nèi)完成部分屬性持久化)。 建議15:break萬萬不可忘; (switch語句中,每一個case匹配完都需要使用break關(guān)鍵字跳出,否則會依次執(zhí)行完所有的case內(nèi)容。)。 建議16:易變業(yè)務(wù)使用腳本語言編寫; (腳本語言:都是在運行期解釋執(zhí)行。腳本語言三大特性:1、靈活:動態(tài)類型;2、便捷:解釋型語言,不需要編譯成二進(jìn)制,不需要像Java一樣生成字節(jié)碼,依靠解釋執(zhí)行,做到不停止應(yīng)用變更代碼;3、簡單:部分簡單。Java使用ScriptEngine執(zhí)行引擎來執(zhí)行JavaScript腳本代碼)。 建議17:慎用動態(tài)編譯; (好處:更加自如地控制編譯過程。很少使用,原因:靜態(tài)編譯能夠完成大部分工作甚至全部,即使需要使用,也有很好的替代方案,如JRuby、Groovy等無縫的腳本語言。動態(tài)編譯注意以下4點:1、在框架中謹(jǐn)慎使用:debug困難,成本大;2、不要在要求高性能的項目中使用:需要一個編譯過程,比靜態(tài)編譯多了一個執(zhí)行環(huán)節(jié);3、動態(tài)編譯要考慮安全問題:防止惡意代碼;4、記錄動態(tài)編譯過程)。 建議18:避免instanceof非預(yù)期結(jié)果; (instanceof用來判斷一個對象是否是一個類的實例,只能用于對象的判斷,不能用于基本類型的判斷(編譯不通過),instanceof操作符的左右操作數(shù)必須有繼承或?qū)崿F(xiàn)關(guān)系,否則編譯會失敗。例:null instanceof String返回值是false,instanceof特有規(guī)則,若左操作數(shù)是null,結(jié)果就直接返回false,不再運算右操作數(shù)是什么類)。 建議19:斷言絕對不是雞肋; (防御式編程中經(jīng)常使用斷言(Assertion)對參數(shù)和環(huán)境做出判斷。斷言是為調(diào)試程序服務(wù)的。兩個特性:1、默認(rèn)assert不啟用;2、assert拋出的異常AssertionError是繼承自Error的)。 建議20:不要只替換一個類; (發(fā)布應(yīng)用系統(tǒng)時禁止使用類文件替換方式,整體WAR包發(fā)布才是完全之策)(Client類中調(diào)用了Constant類中的屬性值,如果更改了Constant常量類屬性的值,重新編譯替換。而不改變或者替換Client類,則Client中調(diào)用的Constant常量類的屬性值并不會改變。原因:對于final修飾的基本類型和String類型,編譯器會認(rèn)為它是穩(wěn)定態(tài)(Immutable Status),所以在編譯時就直接把值編譯到字節(jié)碼中了,避免了再運行期引用,以提高代碼的執(zhí)行效率。而對于final修飾的類(即非基本類型),編譯器認(rèn)為它是不穩(wěn)定態(tài)(Mutable Status),在編譯時建立的則是引用關(guān)系(該類型也叫作Soft Final),如果Client類引入的常量是一個類或?qū)嵗?,即使不重新編譯也會輸出最新值)。 第二章 基本類型 建議21:用偶判斷,不用奇判斷; (不要使用奇判斷(i%2 == 1 ? "奇數(shù)" : "偶數(shù)"),使用偶判斷(i%2 == 0 ? "偶數(shù)" : "奇數(shù)")。原因Java中的取余(%標(biāo)識符)算法:測試數(shù)據(jù)輸入1 2 0 -1 -2,奇判斷的時候,當(dāng)輸入-1時,也會返回偶數(shù)。
建議22:用整數(shù)類型處理貨幣; (不要使用float或者double計算貨幣,因為在計算機(jī)中浮點數(shù)“有可能”是不準(zhǔn)確的,它只能無限接近準(zhǔn)確值,而不能完全精確。不能使用計算機(jī)中的二進(jìn)制位來表示如0.4等的浮點數(shù)。解決方案:1、使用BigDecimal(優(yōu)先使用);2、使用整型)。 建議23:不要讓類型默默轉(zhuǎn)換; (基本類型轉(zhuǎn)換時,使用主動聲明方式減少不必要的Bug)
以上兩句在參與運算時會溢出,因為Java是先運算后再進(jìn)行類型轉(zhuǎn)換的。因為dis2的三個運算參數(shù)都是int類型,三者相乘的結(jié)果也是int類型,但是已經(jīng)超過了int的最大值,所以越界了。解決方法,在運算參數(shù)60后加L即可。 建議24:邊界、邊界、還是邊界; (數(shù)字越界是檢驗條件失效,邊界測試;檢驗條件if(order>0 && order+cur<=LIMIT),輸入的數(shù)大于0,加上cur的值之后溢出為負(fù)值,小于LIMIT,所以滿足條件,但不符合要求)。 建議25:不要讓四舍五入虧了一方; (Math.round(10.5)輸出結(jié)果11;Math.round(-10.5)輸出結(jié)果-10。這是因為Math.round采用的舍入規(guī)則所決定的(采用的是正無窮方向舍入規(guī)則),根據(jù)不同的場景,慎重選擇不同的舍入模式,以提高項目的精準(zhǔn)度,減少算法損失)。 建議26:提防包裝類型的null值; (泛型中不能使用基本類型,只能使用包裝類型,null執(zhí)行自動拆箱操作會拋NullPointerException異常,因為自動拆箱是通過調(diào)用包裝對象的intValue方法來實現(xiàn)的,而訪問null的intValue方法會報空指針異常。謹(jǐn)記一點:包裝類參與運算時,要做null值校驗,即(i!=null ? i : 0))。 建議27:謹(jǐn)慎包裝類型的大小比較; (大于>或者小于<比較時,包裝類型會調(diào)用intValue方法,執(zhí)行自動拆箱比較。而==等號用來判斷兩個操作數(shù)是否有相等關(guān)系的,如果是基本類型則判斷數(shù)值是否相等,如果是對象則判斷是否是一個對象的兩個引用,也就是地址是否相等。通過兩次new操作產(chǎn)生的兩個包裝類型,地址肯定不相等)。 建議28:優(yōu)先使用整型池; (自動裝箱是通過調(diào)用valueOf方法來實現(xiàn)的,包裝類的valueOf生成包裝實例可以顯著提高空間和時間性能)valueOf方法實現(xiàn)源碼:
cache是IntegerCache內(nèi)部類的一個靜態(tài)數(shù)組,容納的是-128到127之間的Integer對象。通過valueOf產(chǎn)生包裝對象時,如果int參數(shù)在-128到127之間,則直接從整型池中獲得對象,不在該范圍的int類型則通過new生成包裝對象。在判斷對象是否相等的時候,最好是利用equals方法,避免“==”產(chǎn)生非預(yù)期結(jié)果。 建議29:優(yōu)先選擇基本類型; (int參數(shù)先加寬轉(zhuǎn)變成long型,然后自動轉(zhuǎn)換成Long型。Integer.valueOf(i)參數(shù)先自動拆箱轉(zhuǎn)變?yōu)閕nt類型,與之前類似)。 建議30:不要隨便設(shè)置隨機(jī)種子; (若非必要,不要設(shè)置隨機(jī)數(shù)種子)(Random r = new Random(1000);該代碼中1000即為隨機(jī)種子。在同一臺機(jī)器上,不管運行多少次,所打印的隨機(jī)數(shù)都是相同的。在Java中,隨機(jī)數(shù)的產(chǎn)生取決于種子,隨機(jī)數(shù)和種子之間的關(guān)系遵從以下兩個規(guī)則:1、種子不同,產(chǎn)生不同的隨機(jī)數(shù);2、種子相同,即使實例不同也產(chǎn)生相同的隨機(jī)數(shù)。Random類默認(rèn)種子(無參構(gòu)造)是System.nanoTime()的返回值,這個值是距離某一個固定時間點的納秒數(shù),所以可以產(chǎn)生隨機(jī)數(shù)。java.util.Random類與Math.random方法原理相同)。 第三章 類、對象及方法 建議31:在接口中不要存在實現(xiàn)代碼; (可以通過在接口中聲明一個靜態(tài)常量s,其值是一個匿名內(nèi)部類的實例對象,可以實現(xiàn)接口中存在實現(xiàn)代碼)。 建議32:靜態(tài)變量一定要先聲明后賦值; (也可以先使用后聲明,因為靜態(tài)變量是類初始化時首先被加載,JVM會去查找類中所有的靜態(tài)聲明,然后分配空間,分配到數(shù)據(jù)區(qū)(Data Area)的,它在內(nèi)存中只有一個拷貝,不會被分配多次,注意這時候只是完成了地址空間的分配還沒有賦值,之后JVM會根據(jù)類中靜態(tài)賦值(包括靜態(tài)類賦值和靜態(tài)塊賦值)的先后順序來執(zhí)行,后面的操作都是地址不變,值改變)。 建議33:不要覆寫靜態(tài)方法; (一個實例對象有兩個類型:表面類型和實際類型,表面類型是聲明時的類型,實際類型是對象產(chǎn)生時的類型。對于非靜態(tài)方法,它是根據(jù)對象的實際類型來執(zhí)行的,即執(zhí)行了覆寫方法。而對于靜態(tài)方法,首先靜態(tài)方法不依賴實例對象,通過類名訪問;其次,可以通過對象訪問靜態(tài)方法,如果通過對象訪問,JVM則會通過對象的表面類型查找到靜態(tài)方法的入口,繼而執(zhí)行)。 建議34:構(gòu)造函數(shù)盡量簡化; (通過new關(guān)鍵字生成對象時必然會調(diào)用構(gòu)造函數(shù)。子類實例化時,首先會初始化父類(注意這里是初始化,可不是生成父類對象),也就是初始化父類的變量,調(diào)用父類的構(gòu)造函數(shù),然后才會初始化子類的變量,調(diào)用子類自己的構(gòu)造函數(shù),最后生成一個實例對象。構(gòu)造函數(shù)太復(fù)雜有可能造成,對象使用時還沒完成初始化)。 建議35:避免在構(gòu)造函數(shù)中初始化其他類; (有可能造成不斷的new新對象的死循環(huán),直到棧內(nèi)存被消耗完拋出StackOverflowError異常為止)。 建議36:使用構(gòu)造代碼塊精煉程序; (四種類型的代碼塊:1、普通代碼塊:在方法后面使用“{}”括起來的代碼片段;2、靜態(tài)代碼塊:在類中使用static修飾,并使用“{}”括起來的代碼片段;3、同步代碼塊:使用synchronized關(guān)鍵字修飾,并使用“{}”括起來的代碼片段,表示同一時間只能有一個縣城進(jìn)入到該方法;4、構(gòu)造代碼塊:在類中沒有任何的前綴或后綴,并使用“{}”括起來的代碼片段。編譯器會把構(gòu)造代碼塊插入到每個構(gòu)造函數(shù)的最前端。構(gòu)造代碼塊的兩個特性:1、在每個構(gòu)造函數(shù)中都運行;2、在構(gòu)造函數(shù)中它會首先運行)。 建議37:構(gòu)造代碼塊會想你所想; (編譯器會把構(gòu)造代碼塊插入到每一個構(gòu)造函數(shù)中,有一個特殊情況:如果遇到this關(guān)鍵字(也就是構(gòu)造函數(shù)調(diào)用自身其他的構(gòu)造函數(shù)時)則不插入構(gòu)造代碼塊。如果遇到super關(guān)鍵字,編譯器會把構(gòu)造代碼塊插入到super方法之后執(zhí)行)。 建議38:使用靜態(tài)內(nèi)部類提高封裝性; (Java嵌套內(nèi)分為兩種:1、靜態(tài)內(nèi)部類;2、內(nèi)部類;靜態(tài)內(nèi)部類兩個優(yōu)點:加強了類的封裝性和提高了代碼的可讀性。靜態(tài)內(nèi)部類與普通內(nèi)部類的區(qū)別:1、靜態(tài)內(nèi)部類不持有外部類的引用,在普通內(nèi)部類中,我們可以直接訪問外部類的屬性、方法,即使是private類型也可以訪問,這是因為內(nèi)部類持有一個外部類的引用,可以自由訪問。而靜態(tài)內(nèi)部類,則只可以訪問外部類的靜態(tài)方法和靜態(tài)屬性,其他則不能訪問。2、靜態(tài)內(nèi)部類不依賴外部類,普通內(nèi)部類與外部類之間是相互依賴的關(guān)系,內(nèi)部類不能脫離外部類實例,同聲同死,一起聲明,一起被垃圾回收器回收。而靜態(tài)內(nèi)部類可以獨立存在,即使外部類消亡了;3、普通內(nèi)部類不能聲明static的方法和變量,注意這里說的是變量,常量(也就是final static修飾的屬性)還是可以的,而靜態(tài)內(nèi)部類形似外部類,沒有任何限制)。 建議39:使用匿名類的構(gòu)造函數(shù); (List l2 = new ArrayList(){}; //定義了一個繼承于ArrayList的匿名類,只是沒有任何的覆寫方法而已
建議40:匿名類的構(gòu)造函數(shù)很特殊; (匿名類初始化時直接調(diào)用了父類的同參數(shù)構(gòu)造器,然后再調(diào)用自己的構(gòu)造代碼塊) 建議41:讓多重繼承成為現(xiàn)實; (Java中一個類可以多種實現(xiàn),但不能多重繼承。使用成員內(nèi)部類實現(xiàn)多重繼承。內(nèi)部類一個重要特性:內(nèi)部類可以繼承一個與外部類無關(guān)的類,保證了內(nèi)部類的獨立性,正是基于這一點,多重繼承才會成為可能)。 建議42:讓工具類不可實例化; (工具類的方法和屬性都是靜態(tài)的,不需要實例即可訪問。實現(xiàn)方式:將構(gòu)造函數(shù)設(shè)置為private,并且在構(gòu)造函數(shù)中拋出Error錯誤異常)。 建議43:避免對象的淺拷貝; (淺拷貝存在對象屬性拷貝不徹底的問題。對于只包含基本數(shù)據(jù)類型的類可以使用淺拷貝;而包含有對象變量的類需要使用序列化與反序列化機(jī)制實現(xiàn)深拷貝)。 建議44:推薦使用序列化實現(xiàn)對象的拷貝; (通過序列化方式來處理,在內(nèi)存中通過字節(jié)流的拷貝來實現(xiàn)深拷貝。使用此方法進(jìn)行對象拷貝時需注意兩點:1、對象的內(nèi)部屬性都是可序列化的;2、注意方法和屬性的特殊修飾符,比如final、static、transient變量的序列化問題都會影響拷貝效果。一個簡單辦法,使用Apache下的commons工具包中的SerializationUtils類,直接使用更加簡潔方便)。 建議45:覆寫equals方法時不要識別不出自己; (需要滿足p.equals(p)放回為真,自反性)。 建議46:equals應(yīng)該考慮null值情景; (覆寫equals方法時需要判一下null,否則可能產(chǎn)生NullPointerException異常)。 建議47:在equals中使用getClass進(jìn)行類型判斷; (使用getClass方法來代替instanceof進(jìn)行類型判斷)。 建議48:覆寫equals方法必須覆寫hashCode方法; (需要兩個相同對象的hashCode方法返回值相同,所以需要覆寫hashCode方法,如果不覆寫,兩個不同對象的hashCode肯定不一樣,簡單實現(xiàn)hashCode方法,調(diào)用org.apache.commons.lang.builder包下的Hash碼生成工具HashCodeBuilder)。 建議49:推薦覆寫toString方法; (原始toString方法顯示不人性化)。 建議50:使用package-info類為包服務(wù); (package-info類是專門為本包服務(wù)的,是一個特殊性主要體現(xiàn)在3個方面:1、它不能隨便被創(chuàng)建;2、它服務(wù)的對象很特殊;3、package-info類不能有實現(xiàn)代碼;package-info類的作用:1、聲明友好類和包內(nèi)訪問常量;2、為在包上標(biāo)注注解提供便利;3、提供包的整體注釋說明)。 建議51:不要主動進(jìn)行垃圾回收; (主動進(jìn)行垃圾回收是一個非常危險的動作,因為System.gc要停止所有的響應(yīng)(Stop 天河world),才能檢查內(nèi)存中是否有可回收的對象,所有的請求都會暫停)。 第四章 字符串 建議52:推薦使用String直接量賦值; (一般對象都是通過new關(guān)鍵字生成,String還有第二種生成方式,即直接聲明方式,如String str = "a";String中極力推薦使用直接聲明的方式,不建議使用new String("a")的方式賦值。原因:直接聲明方式:創(chuàng)建一個字符串對象時,首先檢查字符串常量池中是否有字面值相等的字符串,如果有,則不再創(chuàng)建,直接返回池中引用,若沒有則創(chuàng)建,然后放到池中,并返回新建對象的引用。使用new String()方式:直接聲明一個String對象是不檢查字符串常量池的,也不會吧對象放到池中。String的intern方法會檢查當(dāng)前的對象在對象池中是否有字面值相同的引用對象,有則返回池中對象,沒有則放置到對象池中,并返回當(dāng)前對象)。 建議53:注意方法中傳遞的參數(shù)要求; (replaceAll方法傳遞的第一個參數(shù)是正則表達(dá)式)。 建議54:正確使用String、StringBuffer、StringBuilder; (String使用“+”進(jìn)行字符串連接,之前連接之后會產(chǎn)生一個新的對象,所以會不斷的創(chuàng)建新對象,優(yōu)化之后與StringBuilder和StringBuffer采用同樣的append方法進(jìn)行連接,但是每一次字符串拼接都會調(diào)用一次toString方法,所以會很耗時。StringBuffer與StringBuilder基本相同,只是一個字符數(shù)組的在擴(kuò)容而已,都是可變字符序列,不同點是:StringBuffer是線程安全的,StringBuilder是線程不安全的)。 建議55:注意字符串的位置; (在“+”表達(dá)式中,String字符串具有最高優(yōu)先級)(Java對加號“+”的處理機(jī)制:在使用加號進(jìn)行計算的表達(dá)式中,只要遇到String字符串,則所有的數(shù)據(jù)都會轉(zhuǎn)換為String類型進(jìn)行拼接,如果是原始數(shù)據(jù),則直接拼接,如果是對象,則調(diào)用toString方法的返回值然后拼接)。 建議56:自由選擇字符串拼接方式; (字符串拼接有三種方法:加號、concat方法及StringBuilder(或StringBuffer)的append方法。字符串拼接性能中,StringBuilder的append方法最快,concat方法次之,加號最慢。原因:1、“+”方法拼接字符串:雖然編譯器對字符串的加號做了優(yōu)化,使用StringBuidler的append方法進(jìn)行追加,但是與純粹使用StringBuilder的append方法不同:一是每次循環(huán)都會創(chuàng)建一個StringBuilder對象,二是每次執(zhí)行完都要調(diào)用toString方法將其準(zhǔn)換為字符串--toString方法最耗時;2、concat方法拼接字符串:就是一個數(shù)組拷貝,但是每次的concat操作都會新創(chuàng)建一個String對象,這就是concat速度慢下來的真正原因;3、append方法拼接字符串:StringBuidler的append方法直接由父類AbstractStringBuilder實現(xiàn),整個append方法都在做字符數(shù)組處理,沒有新建任何對象,所以速度快)。 建議57:推薦在復(fù)雜字符串操作中使用正則表達(dá)式; (正則表達(dá)式是惡魔,威力強大,但難以控制)。 建議58:強烈建議使用UTF編碼; (一個系統(tǒng)使用統(tǒng)一的編碼)。 建議59:對字符串排序持一種寬容的心態(tài); (如果排序不是一個關(guān)鍵算法,使用Collator類即可。主要針對于中文)。 第五章 數(shù)組和集合 建議60:性能考慮,數(shù)組是首選; (性能要求較高的場景中使用數(shù)組替代集合)(基本類型在棧內(nèi)存中操作,對象在堆內(nèi)存中操作。數(shù)組中使用基本類型是效率最高的,使用集合類會伴隨著自動裝箱與自動拆箱動作,所以性能相對差一些)。 建議61:若有必要,使用變長數(shù)組; (使用Arrays.copyOf(datas,newLen)對原數(shù)組datas進(jìn)行擴(kuò)容處理)。 建議62:警惕數(shù)組的淺拷貝; (通過Arrays.copyOf(box1,box1.length)方法產(chǎn)生的數(shù)組是一個淺拷貝,這與序列化的淺拷貝完全相同:基本類型是直接拷貝值,其他都是拷貝引用地址。數(shù)組中的元素沒有實現(xiàn)Serializable接口)。 建議63:在明確的場景下,為集合指定初始容量; (ArrayList集合底層使用數(shù)組存儲,如果沒有初始為ArrayList指定數(shù)組大小,默認(rèn)存儲數(shù)組大小長度為10,添加的元素達(dá)到數(shù)組臨界值后,使用Arrays.copyOf方法進(jìn)行1.5倍擴(kuò)容處理。HashMap是按照倍數(shù)擴(kuò)容的,Stack繼承自Vector,所采用擴(kuò)容規(guī)則的也是翻倍)。 建議64:多種最值算法,適時選擇; (最值計算時使用集合最簡單,使用數(shù)組性能最優(yōu),利用Set集合去重,使用TreeSet集合自動排序)。 建議65:避開基本類型數(shù)組轉(zhuǎn)換列表陷阱; (原始類型數(shù)組不能作為asList的輸入?yún)?shù),否則會引起程序邏輯混亂)(基本類型是不能泛化的,在java中數(shù)組是一個對象,它是可以泛化的。使用Arrays.asList(data)方法傳入一個基本類型數(shù)組時,會將整個基本類型數(shù)組作為一個數(shù)組對象存入,所以存入的只會是一個對象。JVM不可能輸出Array類型,因為Array是屬于java.lang.reflect包的,它是通過反射訪問數(shù)組元素的工具類。在Java中任何一個數(shù)組的類都是“[I”,因為Java并沒有定義數(shù)組這個類,它是編譯器編譯的時候生成的,是一個特殊的類)。 建議66:asList方法產(chǎn)生的List對象不可更改; (使用add方法向asList方法生成的集合中添加元素時,會拋UnsupportedOperationException異常。原因:asList生成的ArrayList集合并不是java.util.ArrayList集合,而是Arrays工具類的一個內(nèi)置類,我們經(jīng)常使用的List.add和List.remove方法它都沒有實現(xiàn),也就是說asList返回的是一個長度不可變的列表。此處的列表只是數(shù)組的一個外殼,不再保持列表動態(tài)變長的特性)。 建議67:不同的列表選擇不同的遍歷方法; (ArrayList數(shù)組實現(xiàn)了RandomAccess接口(隨機(jī)存取接口),ArrayList是一個可以隨機(jī)存取的列表。集合底層如果是基于數(shù)組實現(xiàn)的,實現(xiàn)了RandomAccess接口的集合,使用下標(biāo)進(jìn)行遍歷訪問性能會更高;底層使用雙向鏈表實現(xiàn)的集合,使用foreach的迭代器遍歷性能會更高)。 建議68:頻繁插入和刪除時使用LinkedList; (ArrayList集合,每次插入或者刪除一個元素,其后的所有元素就會向后或者向前移動一位,性能很低。LinkedList集合插入時不需要移動其他元素,性能高;修改元素,LinkedList集合比ArrayList集合要慢很多;添加元素,LinkedList與ArrayList集合性能差不多,LinkedList添加一個ListNode,而ArrayList則在數(shù)組后面添加一個Entry)。 建議69:列表相等只需關(guān)心元素數(shù)據(jù); (判斷集合是否相等時只須關(guān)注元素是否相等即可)(ArrayList與Vector都是List,都實現(xiàn)了List接口,也都繼承了AbstractList抽象類,其equals方法是在AbstractList中定義的。所以只要求兩個集合類實現(xiàn)了List接口就成,不關(guān)心List的具體實現(xiàn)類,只要所有的元素相等,并且長度也相等就表明兩個List是相等的,與具體的容量類型無關(guān))。 建議70:子列表只是原列表的一個視圖; (使用==判斷相等時,需要滿足兩個對象地址相等,而使用equals判斷兩個對象是否相等時,只需要關(guān)注表面值是否相等。subList方法是由AbstractList實現(xiàn)的,它會根據(jù)是不是可以隨機(jī)存取來提供不同的SubList實現(xiàn)方式,RandomAccessSubList是SubList子類,SubList類中subList方法的實現(xiàn)原理:它返回的SubList類是AbstractList的子類,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并沒有生成一個數(shù)組或是鏈表,也就是子列表只是原列表的一個視圖,所有的修改動作都反映在了原列表上)。 建議71:推薦使用subList處理局部列表; (需求:要刪除一個ArrayList中的20-30范圍內(nèi)的元素;將原列表轉(zhuǎn)換為一個可變列表,然后使用subList獲取到原列表20到30范圍內(nèi)的一個視圖(View),然后清空該視圖內(nèi)的元素,即可在原列表中刪除20到30范圍內(nèi)的元素)。 建議72:生成子列表后不要再操作原列表; (subList生成子列表后,使用Collections.unmodifiableList(list);保持原列表的只讀狀態(tài))(利用subList生成子列表后,更改原列表,會造成子列表拋出java.util.ConcurrentModificationException異常。原因:subList取出的列表是原列表的一個視圖,原數(shù)據(jù)集(代碼中的list變量)修改了,但是subList取出的子列表不會重新生成一個新列表(這點與數(shù)據(jù)庫視圖是不相同的),后面再對子列表操作時,就會檢測到修改計數(shù)器與預(yù)期的不相同,于是就拋出了并發(fā)修改異常)。 建議73:使用Comparator進(jìn)行排序; (Comparable接口可以作為實現(xiàn)類的默認(rèn)排序法,Comparator接口則是一個類的擴(kuò)展排序工具)(兩種數(shù)據(jù)排序?qū)崿F(xiàn)方式:1、實現(xiàn)Comparable接口,必須要實現(xiàn)compareTo方法,一般由類直接實現(xiàn),表明自身是可比較的,有了比較才能進(jìn)行排序;2、實現(xiàn)Comparator接口,必須實現(xiàn)compare方法,Comparator接口是一個工具類接口:用作比較,它與原有類的邏輯沒有關(guān)系,只是實現(xiàn)兩個類的比較邏輯)。 建議74:不推薦使用binarySearch對列表進(jìn)行檢索; (indexOf與binarySearch方法功能類似,只是使用了二分法搜索。使用二分查找的首要條件是必須要先排序,不然二分查找的值是不準(zhǔn)確的。indexOf方法直接就是遍歷搜尋。從性能方面考慮,binarySearch是最好的選擇)。 建議75:集合中的元素必須做到compareTo和equals同步; (實現(xiàn)了compareTo方法,就應(yīng)該覆寫equals方法,確保兩者同步)(在集合中indexOf方法是通過equals方法的返回值判斷的,而binarySearch查找的依據(jù)是compareTo方法的返回值;equals是判斷元素是否相等,compareTo是判斷元素在排序中的位置是否相同)。 建議76:集合運算時使用更優(yōu)雅的方式; (1、并集:list1.addAll(list2); 2、交集:list1.retainAll(list2); 3、差集:list1.removeAll(list2); 4、無重復(fù)的并集:list2.removeAll(list1);list1.addAll(list2);)。 建議77:使用shuffle打亂列表; (使用Collections.shuffle(tagClouds)打亂列表)。 建議78:減少HashMap中元素的數(shù)量; (盡量讓HashMap中的元素少量并簡單)(現(xiàn)象:使用HashMap存儲數(shù)據(jù)時,還有空閑內(nèi)存,卻拋出了內(nèi)存溢出異常;原因:HashMap底層的數(shù)組變量名叫table,它是Entry類型的數(shù)組,保存的是一個一個的鍵值對。與ArrayList集合相比,HashMap比ArrayList多了一次封裝,把String類型的鍵值對轉(zhuǎn)換成Entry對象后再放入數(shù)組,這就多了40萬個對象,這是問題產(chǎn)生的第一個原因;HashMap在插入鍵值對時,會做長度校驗,如果大于或等于閾值(threshold變量),則數(shù)組長度增大一倍。默認(rèn)閾值是當(dāng)前長度與加載因子的乘積,默認(rèn)的加載因子(loadFactor變量)是0.75,也就是說只要HashMap的size大于數(shù)組長度的0.75倍時,就開始擴(kuò)容。導(dǎo)致到最后,空閑的內(nèi)存空間不足以增加一次擴(kuò)容時就會拋出OutOfMemoryError異常)。 建議79:集合中的哈希碼不要重復(fù); (列表查找不管是遍歷查找、鏈表查找或者是二分查找都不夠快。最快的是Hash開頭的集合(如HashMap、HashSet等類)查找,原理:根據(jù)hashCode定位元素在數(shù)組中的位置。HashMap的table數(shù)組存儲元素特點:1、table數(shù)組的長度永遠(yuǎn)是2的N次冪;2、table數(shù)組中的元素是Entry類型;3、table數(shù)組中的元素位置是不連續(xù)的;每個Entry都有一個next變量,它會指向下一個鍵值對,用來鏈表的方式來處理Hash沖突的問題。如果Hash碼相同,則添加的元素都使用鏈表處理,在查找的時候這部分的性能與ArrayList性能差不多)。 建議80:多線程使用Vector或HashTable; (Vector與ArrayList原理類似,只是是線程安全的,HashTable是HashMap的多線程版本。線程安全:基本所有的集合類都有一個叫快速失敗(Fail-Fast)的校驗機(jī)制,當(dāng)一個集合在被多個線程修改并訪問時,就可能出現(xiàn)ConcurrentModificationException異常,這是為了確保集合方法一致而設(shè)置的保護(hù)措施;實現(xiàn)原理是modCount修改計數(shù)器:如果在讀列表時,modCount發(fā)生變化(也就是有其他線程修改)則會拋出ConcurrentModificationException異常。線程同步:是為了保護(hù)集合中的數(shù)據(jù)不被臟讀、臟寫而設(shè)置的)。 建議81:非穩(wěn)定排序推薦使用List; (非穩(wěn)定的意思是:經(jīng)常需要改動;TreeSet集合中元素不可重復(fù),且默認(rèn)按照升序排序,是根據(jù)Comparable接口的compareTo方法的返回值確定排序位置的。SortedSet接口(TreeSet實現(xiàn)了該接口)只是定義了在該集合加入元素時將其進(jìn)行排序,并不能保證元素修改后的排序結(jié)果。因此TreeSet適用于不變量的集合數(shù)據(jù)排序,但不適合可變量的排序。對于可變量的集合,需要自己手動進(jìn)行再排序)(SortedSet中的元素被修改后可能會影響其排序位置)。 建議82:由點及面,一頁知秋--集合大家族; (1、List:實現(xiàn)List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一個動態(tài)數(shù)組,LinkedList是一個雙向鏈表,Vector是一個線程安全的動態(tài)數(shù)組,Stack是一個對象棧,遵循先進(jìn)后出的原則; 2、Set:Set是不包含重復(fù)元素的集合,其主要的實現(xiàn)類有:EnumSet、HashSet、TreeSet,其中EnumSet是枚舉類型的專用Set,HashSet是以哈希碼決定其元素位置的Set,原理與HashMap相似,提供快速插入與查找方法,TreeSet是一個自動排序的Set,它實現(xiàn)了SortedSet接口; 3、Map:可以分為排序Map和非排序Map;排序Map為TreeMap,根據(jù)Key值進(jìn)行自動排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子類,EnumMap則要求其Key必須是某一個枚舉類型; 4:Queue:分為兩類,一類是阻塞式隊列,隊列滿了以后再插入元素會拋異常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是以數(shù)組方式實現(xiàn)的有界阻塞隊列;PriorityBlockingQueue是依照優(yōu)先級組件的隊列;LinkedBlockingQueue是通過鏈表實現(xiàn)的阻塞隊列;另一類是非阻塞隊列,無邊界的,只要內(nèi)存允許,都可以追加元素,經(jīng)常使用的是PriorityQueue類。還有一種是雙端隊列,支持在頭、尾兩端插入和移除元素,主要實現(xiàn)類是:ArrayDeque、LinkedBlockingDeque、LinkedList; 5、數(shù)組:數(shù)組能存儲基本類型,而集合不行;所有的集合底層存儲的都是數(shù)組; 6、工具類:數(shù)組的工具類是:java.util.Arrays和java.lang.reflect.array;集合的工具類是java.util.Collections; 7、擴(kuò)展類:可以使用Apache的commons-collections擴(kuò)展包,也可以使用Google的google-collections擴(kuò)展包)。 第六章 枚舉和注解 建議83:推薦使用枚舉定義常量; (在項目開發(fā)中,推薦使用枚舉常量替代接口常量和類常量)(常量分為:類常量、接口常量、枚舉常量;枚舉常量優(yōu)點:1、枚舉常量更簡單;2、枚舉常量屬于穩(wěn)態(tài)性(不允許發(fā)生越界);3、枚舉具有內(nèi)置方法,values方法可以獲取到所有枚舉值;4、枚舉可以自定義方法)。 建議84:使用構(gòu)造函數(shù)協(xié)助描述枚舉項; (每個枚舉項都是該枚舉的一個實例。可以通過添加屬性,然后通過構(gòu)造函數(shù)給枚舉項添加更多描述信息)。 建議85:小心switch帶來的空值異常; (使用枚舉值作為switch(枚舉類);語句的條件值時,需要對枚舉類進(jìn)行判斷是否為null值。因為Java中的switch語句只能判斷byte、short、char、int類型,JDK7可以判斷String類型,使用switch語句判斷枚舉類型時,會根據(jù)枚舉的排序值匹配。如果傳入的只是null的話,獲取排序值需要調(diào)用如season.ordinal()方法時會拋出NullPointerException異常)。 建議86:在switch的default代碼塊中增加AssertionError錯誤; (switch語句在使用枚舉類作為判斷條件時,避免出現(xiàn)增加了一個枚舉項,而switch語句沒做任何修改,編譯不會出現(xiàn)問題,但是在運行期會發(fā)生非預(yù)期的錯誤。為避免這種情況出現(xiàn),建議在default后直接拋出一個AssertionError錯誤。含義是:不要跑到這里來,一跑到這里來馬上就會報錯)。 建議87:使用valueOf前必須進(jìn)行校驗; (Enum.valueOf()方法會把一個String類型的名稱轉(zhuǎn)變?yōu)槊杜e項,也就是在枚舉項中查找出字面值與該參數(shù)相等的枚舉項。valueOf方法先通過反射從枚舉類的常量聲明中查找,若找到就直接返回,若找不到就拋出IllegalArgumentException異常)。 建議88:用枚舉實現(xiàn)工廠方法模式更簡潔; (工廠方法模式是“創(chuàng)建對象的接口,讓子類決定實例化哪一個類,并使一個類的實例化延遲到其子類”。枚舉實現(xiàn)工廠方法模式有兩種方法:1、枚舉非靜態(tài)方法實現(xiàn)工廠方法模式;2、通過抽象方法生成產(chǎn)品;優(yōu)點:1、避免錯誤調(diào)用的發(fā)生;2、性能好,使用便捷;3、減低類間耦合性)。 建議89:枚舉項的數(shù)量控制在64個以內(nèi); (Java提供了兩個枚舉集合:EnumSet、EnumMap;EnumSet要求其元素必須是某一枚舉的枚舉項,EnumMap表示Key值必須是某一枚舉的枚舉項。由于枚舉類型的實例數(shù)量固定并且有限,相對來說EnumSet和EnumMap的效率會比其他Set和Map要高。Java處理EnumSet過程:當(dāng)枚舉項小于等于64時,創(chuàng)建一個RegularEnumSet實例對象,大于64時創(chuàng)一個JumboEnumSet實例對象。RegularEnumSet是把每個枚舉項編碼映射到一個long類型數(shù)字得每一位上,而JumboEnumSet則會先按照64個一組進(jìn)行拆分,然后每個組再映射到一個long類型的數(shù)字得每一位上)。 建議90:小心注解繼承; (不常用的元注解(Meta-Annotation):@Inherited,它表示一個注解是否可以自動被繼承)。 建議91:枚舉和注解結(jié)合使用威力更大; (注解和接口寫法類似,都采用了關(guān)鍵字interface,而且都不能有實現(xiàn)代碼,常量定義默認(rèn)都是public static final類型的等,他們的主要不同點:注解要在interface前加上@字符,而且不能繼承,不能實現(xiàn))。 建議92:注意@Override不同版本的區(qū)別; (@Override注解用于方法的覆寫上,它在編譯期有效,也就是Java編譯器在編譯時會根據(jù)該注解檢查方法是否真的是覆寫,如果不是就報錯,拒絕編譯。Java1.5版本中@Override是嚴(yán)格遵守覆寫的定義:子類方法與父類方法必須具有相同的方法名、輸入?yún)?shù)、輸出參數(shù)(允許子類縮小)、訪問權(quán)限(允許子類擴(kuò)大),父類必須是一個類,不是是接口,否則不能算是覆寫。而在Java1.6就開放了很多,實現(xiàn)接口的方法也可以加上@Override注解了。如果是Java1.6版本移植到Java1.5版本中時,需要刪除接口實現(xiàn)方法上的@Override注解)。 第七章 泛型和反射 建議93:Java的泛型是類型擦除的; (加入泛型優(yōu)點:加強了參數(shù)類型的安全性,減少了類型的轉(zhuǎn)換。Java的泛型在編譯期有效,在運行期被刪除,也就是說所有的泛型參數(shù)類型在編譯后都會被清除掉。所以:1、泛型的class對象時是相同的;2、泛型數(shù)組初始化時不能聲明泛型類型;3、instanceof不允許存在泛型參數(shù))。 建議94:不能初始化泛型參數(shù)和數(shù)組; (泛型類型在編譯期被擦除,在類初始化時將無法獲得泛型的具體參數(shù),所以泛型參數(shù)和數(shù)組無法初始化,但是ArrayList卻可以,因為ArrayList初始化是向上轉(zhuǎn)型變成了Object類型;需要泛型數(shù)組解決辦法:只聲明,不再初始化,由構(gòu)造函數(shù)完成初始化操作)。 建議95:強制聲明泛型的實際類型; (無法從代碼中推斷出泛型類型的情況下,即可強制聲明泛型類型;方法:List<Integer> list2 = ArrayUtils.<Integer>asList();在輸入前定義這是一個Integer類型的參數(shù))。 建議96:不同的場景使用不同的泛型通配符; (Java泛型支持通配符(Wildcard),可以單獨使用一個“?”表示任意類,也可以使用extends關(guān)鍵字表示某一個類(接口)的子類型,還可以使用super關(guān)鍵字表示某一個類(接口)的父類型。1、泛型結(jié)構(gòu)只參與“讀”操作則限定上界(extends關(guān)鍵字);2、泛型結(jié)構(gòu)只參與“寫”操作則限定下界(使用super關(guān)鍵字);3、如果一個泛型結(jié)構(gòu)既用作“讀”操作也用作“寫”操作則使用確定的泛型類型即可,如List<E>)。 建議97:警惕泛型是不能協(xié)變和逆變的; (Java的泛型是不支持協(xié)變和逆變的,只是能夠?qū)崿F(xiàn)協(xié)變和逆變)(協(xié)變和逆變是指寬類型和窄類型在某種情況下(如參數(shù)、泛型、返回值)替換或交換的特性。簡單地說,協(xié)變是用一個窄類型替換寬類型,而逆變則是用寬類型覆蓋窄類型。子類覆寫父類返回值類型比父類型變窄,則是協(xié)變;子類覆寫父類型的參數(shù)類型變寬,則是逆變。數(shù)組支持協(xié)變,泛型不支持協(xié)變)。 建議98:建議采用的順序是List<T>,List<?>,List<Object>; (1、List<T>是確定的某一個類型,編碼者知道它是一個類型,只是在運行期才確定而已;2、List<T>可以進(jìn)行讀寫操作,List<?>是只讀類型,因為編譯器不知道List中容納的是什么類型的元素,無法增加、修改,但是能刪除,List<Object>也可以讀寫操作,只是此時已經(jīng)失去了泛型存在的意義了)。 建議99:嚴(yán)格限定泛型類型采用多重界限; (使用“&”符號連接多個泛型界限,如:<T extends Staff & Passenger>)。 建議100:數(shù)組的真實類型必須是泛型類型的子類型; (有可能會拋出ClassCastException異常,toArray方法返回后會進(jìn)行一次類型轉(zhuǎn)換,Object數(shù)組轉(zhuǎn)換成了String數(shù)組。由于我們無法在運行期獲得泛型類型的參數(shù),因此就需要調(diào)用者主動傳入T參數(shù)類型)。 建議101:注意Class類的特殊性; (Java處理的基本機(jī)制:先把Java源文件編譯成后綴為class的字節(jié)碼文件,然后再通過ClassLoader機(jī)制把這些類文件加載到內(nèi)存中,最后生成實例執(zhí)行。Java使用一個元類(MetaClass)來描述加載到內(nèi)存中的類數(shù)據(jù),這就是Class類,它是一個描述類的類對象。Class類是“類中類”,具有特殊性:1、無構(gòu)造函數(shù),不能實例化,Class對象是在加載類時由Java虛擬機(jī)通過調(diào)用類加載器中的defineClass方法自動構(gòu)建的;2、可以描述基本類型,8個基本類型在JVM中并不是一個對象,一般存在于棧內(nèi)存中,但是Class類仍然可以描述它們,例如可以使用int.class表示int類型的類對象;3、其對象都是單例模式,一個Class的實例對象描述一個類,并且只描述一個類,反過來也成立,一個類只有一個Class實例對象。Class類是Java的反射入口,只有在獲得了一個類的描述對象后才能動態(tài)地加載、調(diào)用,一般獲得一個Class對象有三種途徑:1、類屬性方式,如String.class;2、對象的getClass方法,如new String().getClass();3、forName方法重載,如Class.forName("java.lang.String")。獲得了Class對象后,就可以通過getAnnotation()獲得注解,通過個體Methods()獲得方法,通過getConstructors()獲得構(gòu)造函數(shù)等)。 建議102:適時選擇getDeclaredXXX和getXXX; (getMethod方法獲得的是所有public訪問級別的方法,包括從父類繼承的方法,而getDeclaredMethod獲得的是自身類的所有方法,包括公用方法、私有方法等,而且不受限于訪問權(quán)限。Java之所以這樣處理,是因為反射本意只是正常代碼邏輯的一種補充,而不是讓正常代碼邏輯產(chǎn)生翻天覆地的改動,所以public的屬性和方法最容易獲取,私有屬性和方法也可以獲取,但要限定本類。如果需要列出所有繼承自父類的方法,需要先獲得父類,然后調(diào)用getDeclaredMethods方法,之后持續(xù)遞歸)。 建議103:反射訪問屬性或方法是將Accessible設(shè)置為true; (通過反射方式執(zhí)行方法時,必須在invoke之前檢查Accessible屬性。而Accessible屬性并不是我們語法層級理解的訪問權(quán)限,而是指是否更容易獲得,是否進(jìn)行安全檢查。Accessible屬性只是用來判斷是否需要進(jìn)行安全檢查的,如果不需要則直接執(zhí)行,這就可以大幅度地提升系統(tǒng)性能。經(jīng)過測試,在大量的反射情況下,設(shè)置Accessible為true可以提升性能20倍以上)。 建議104:使用forName動態(tài)加載類文件; (forName只是加載類,并不執(zhí)行任何代碼)(動態(tài)加載(Dynamic Loading)是指在程序運行時加載需要的類庫文件,一般情況下,一個類文件在啟動時或首次初始化時會被加載到內(nèi)存中,而反射則可以在運行時再決定是否要加載一個類,然后在JVM中加載并初始化。動態(tài)加載通常是通過Class.forName(String)實現(xiàn)。一個對象的生成必然會經(jīng)過一下兩個步驟:1、加載到內(nèi)存中生成Class的實例對象;2、通過new關(guān)鍵字生成實例對象;動態(tài)加載的意義:加載一個類即表示要初始化該類的static變量,特別是static代碼塊,在這里我們可以做大量的工作,比如注冊自己,初始化環(huán)境等,這才是我們重點關(guān)注的邏輯)。 建議105:動態(tài)加載不適合數(shù)組; (通過反射操作數(shù)組使用Array類,不要采用通用的反射處理API)(如果forName要加載一個類,那它首先必須是一個類--8個基本類型排除在外,不是具體的類;其次,它必須具有可追索的類路徑,否則會報ClassNotFoundException異常。在Java中,數(shù)組是一個非常特殊的類,雖然是一個類,但沒有定義類路徑。作為forName參數(shù)時會拋出ClassNotFoundException異常,原因是:數(shù)組雖然是一個類,在聲明時可以定義為String[],但編譯器編譯后會為不同的數(shù)組類型生成不同的類,所以要想動態(tài)創(chuàng)建和訪問數(shù)組,基本的反射是無法實現(xiàn)的)。 建議106:動態(tài)代理可以使代理模式更加靈活; (Java的反射框架提供了動態(tài)代理(Dynamic Proxy)機(jī)制,允許在運行期對目標(biāo)類生成代理,避免重復(fù)開發(fā)。靜態(tài)代理是通過代理主題角色(Proxy)和具體主題角色(Real Subject)共同實現(xiàn)抽象主題角色(Subject)的邏輯的,只是代理主題角色把相關(guān)的執(zhí)行邏輯委托給了具體主題角色而已。動態(tài)代理需要實現(xiàn)InvocationHandler接口,必須要實現(xiàn)invoke方法,該方法完成了對真實方法的調(diào)用)。 建議107:使用反射增加裝飾模式的普適性; (裝飾模式(Decorator Pattern)的定義是“動態(tài)地給一個對象添加一些額外的職責(zé)。就增加功能來說,裝飾模式相比于生成子類更為靈活”。比較通用的裝飾模式,只需要定義被裝飾的類及裝飾類即可,裝飾行為由動態(tài)代理實現(xiàn),實現(xiàn)了對裝飾類和被裝飾類的完全解耦,提供了系統(tǒng)的擴(kuò)展性)。 建議108:反射讓模板方法模式更強大; (決定使用模板方法模式時,請嘗試使用反射方式實現(xiàn),它會讓你的程序更靈活、更強大)(模板方法模式(Template Method Pattern)的定義是:定義一個操作中的算法骨架,將一些步驟延遲到子類中,使子類不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。簡單說,就是父類定義抽象模板作為骨架,其中包括基本方法(是由子類實現(xiàn)的方法,并且在模板方法被調(diào)用)和模板方法(實現(xiàn)對基本方法的調(diào)度,完成固定的邏輯),它使用了簡單的繼承和覆寫機(jī)制。使用反射后,不需要定義任何抽象方法,只需定義一個基本方法鑒別器即可加載復(fù)合規(guī)則的基本方法)。 建議109:不需要太多關(guān)注反射效率; (反射效率低是個真命題,但因為這一點而不使用它就是個假命題)(反射效率相對于正常的代碼執(zhí)行確實低很多(經(jīng)測試,相差15倍左右),但是它是一個非常有效的運行期工具類)。 第8章 異常 建議110:提倡異常封裝;(異常封裝有三方面的優(yōu)點:1、提高系統(tǒng)的友好性;2、提高系統(tǒng)的可維護(hù)性;3、解決Java異常機(jī)制本身的缺陷); 建議111:采用異常鏈傳遞異常; (責(zé)任鏈模式(Chain of Responsibility),目的是將多個對象連城一條鏈,并沿著這條鏈傳遞該請求,直到有對象處理它為止,異常的傳遞處理也應(yīng)該采用責(zé)任鏈模式)。 建議112:受檢異常盡可能轉(zhuǎn)化為非受檢異常; (受檢異常威脅到系統(tǒng)的安全性、穩(wěn)定性、可靠性、正確性時、不能轉(zhuǎn)為非受檢異常)(受檢異常(Checked Exception),非受檢異常(Unchecked Exception),受檢異常時正常邏輯的一種補償處理手段,特別是對可靠性要求比較高的系統(tǒng)來說,在某些條件下必須拋出受檢異常以便由程序進(jìn)行補償處理,也就是說受檢異常有合理的存在理由。但是受檢異常有不足的地方:1、受檢異常使接口聲明脆弱;2、受檢異常是代碼的可讀性降低,一個方法增加了受檢異常,則必須有一個調(diào)用者對異常進(jìn)行處理。受檢異常需要try..catch處理;3、受檢異常增加了開發(fā)工作量。避免以上受檢異常缺點辦法:將受檢異常轉(zhuǎn)化為非受檢異常)。 建議113:不要在finally塊中處理返回值; (在finally塊中加入了return語句會導(dǎo)致以下兩個問題:1、覆蓋了try代碼塊中的return返回值;2、屏蔽異常,即使throw出去了異常,異常線程會登記異常,但是當(dāng)執(zhí)行器執(zhí)行finally代碼塊時,則會重新為方法賦值,也就是告訴調(diào)用者“該方法執(zhí)行正確”,沒有發(fā)生異常,于是乎,異常神奇的消失了)。 建議114:不要在構(gòu)造函數(shù)中拋異常; (Java異常機(jī)制有三種:1、Error類及其子類表示的是錯誤,它是不需要程序員處理的也不能處理的異常,比如VirtualMachineError虛擬機(jī)錯誤,ThreadDeath線程僵死等;2、RuntimeException類及其子類表示的是非受檢異常,是系統(tǒng)可能拋出的異常,程序員可以去處理,也可以不處理,最經(jīng)典的是NullPointerException空指針異常和IndexOutOfBoundsException越界異常;3、Exception類及其子類(不包含非受檢異常)表示的是受檢異常,這是程序員必須要處理的異常,不處理則程序不能通過編譯,比如IOException表示I/O異常,SQLException數(shù)據(jù)庫訪問異常。一個對象的創(chuàng)建過程要經(jīng)過內(nèi)存分配、靜態(tài)代碼初始化、構(gòu)造函數(shù)執(zhí)行等過程,構(gòu)造函數(shù)中是否允許拋出異常呢?從Java語法上來說,完全可以,三類異常都可以,但是從系統(tǒng)設(shè)計和開發(fā)的角度分析,則盡量不要在構(gòu)造函數(shù)中拋出異常)。 建議115:使用Throwable獲得棧信息; (在出現(xiàn)異常時(或主動聲明一個Throwable對象時),JVM會通過fillInStackTrace方法記錄下棧信息,然后生成一個Throwable對象,這樣就能知道類間的調(diào)用順序、方法名稱以及當(dāng)前行號等)。 建議116:異常只為異常服務(wù); (異常原本是正常邏輯的一個補充,但有時候會被當(dāng)前主邏輯使用。異常作為主邏輯有問題:1、異常判斷降低了系統(tǒng)性能;2、降低了代碼的可讀性,只有詳細(xì)了解valueOf方法的人才能讀懂這樣的代碼,因為valueOf拋出的是一個非受檢異常;3、隱藏了運行期可能產(chǎn)生的錯誤,catch到異常,但沒有做任何處理)。 建議117:多使用異常,把性能問題放一邊; (new一個IOException會被String慢5倍:因為它要執(zhí)行fillInStackTrace方法,要記錄當(dāng)前棧的快照,而String類則是直接申請一個內(nèi)存創(chuàng)建對象。而且,異常類是不能緩存的。但是異常是主邏輯的例外邏輯,會讓方法更符合實際的處理邏輯,同時使主邏輯更加清晰,可讓正常代碼和異常代碼分離、能快速查找問題(棧信息快照)等)。 第9章 多線程和并發(fā) 建議118:不推薦覆寫start方法; (繼承自Thread類的多線程類不必覆寫start方法。原本的start方法中,調(diào)用了本地方法start0,它實現(xiàn)了啟動線程、申請棧內(nèi)存、運行run方法、修改線程狀態(tài)等職責(zé),線程管理和棧內(nèi)存管理都是由JVM實現(xiàn)的,如果覆蓋了start方法,也就是撤銷了線程管理和棧內(nèi)存管理的能力。所以除非必要,不然不要覆寫start方法,即使需要覆寫start方法,也需要在方法體內(nèi)加上super.start調(diào)用父類中的start方法來啟動默認(rèn)的線程操作)。 建議119:啟動線程前stop方法是不可靠的; (現(xiàn)象:使用stop方法停止一個線程,而stop方法在此處的目的不是停止一個線程,而是設(shè)置線程為不可啟用狀態(tài)。但是運行結(jié)果出現(xiàn)奇怪現(xiàn)象:部分線程還是啟動了,也就是在某些線程(沒有規(guī)律)中的start方法正常執(zhí)行了。在不符合判斷規(guī)則的情況下,不可啟用狀態(tài)的線程還是啟用了,這是線程啟動(start方法)一個缺陷。Thread類的stop方法會根據(jù)線程狀態(tài)來判斷是終結(jié)線程還是設(shè)置線程為不可運行狀態(tài),對于未啟動的線程(線程狀態(tài)為NEW)來說,會設(shè)置其標(biāo)志位為不可啟動,而其他的狀態(tài)則是直接停止。start方法源碼中,start0方法在stop0方法之前,也就是說即使stopBeforeStart為true(不可啟動),也會先啟動一個線程,然后再stop0結(jié)束這個線程,而罪魁禍?zhǔn)拙驮谶@里!所以不要使用stop方法進(jìn)行狀態(tài)的設(shè)置)。 建議120:不適用stop方法停止線程; (線程啟動完畢后,需要停止,Java只提供了一個stop方法,但是不建議使用,有以下三個問題:1、stop方法是過時的;2、stop方法會導(dǎo)致代碼邏輯不完整,stop方法是一種“惡意”的中斷,一旦執(zhí)行stop方法,即終止當(dāng)前正在運行的線程,不管線程邏輯是否完整,這是非常危險的,以為stop方法會清除棧內(nèi)信息,結(jié)束該線程,但是可能該線程的一段邏輯非常重,比如子線程的主邏輯、資源回收、情景初始化等,因為stop線程了,這些都不會再執(zhí)行。子線程執(zhí)行到何處會被關(guān)閉很難定位,這為以后的維護(hù)帶來了很多麻煩;3、stop方法會破壞原子邏輯,多線程為了解決共享資源搶占的問題,使用了鎖概念,避免資源不同步,但是stop方法會丟棄所有的鎖,導(dǎo)致原子邏輯受損。Thread提供的interrupt中斷線程方法,它不能終止一個正在執(zhí)行著的線程,它只是修改中斷標(biāo)志唯一??傊?,期望終止一個正在運行的線程,不能使用stop方法,需要自行編碼實現(xiàn)。如果使用線程池(比如ThreadPoolExecutor類),那么可以通過shutdown方法逐步關(guān)閉池中的線程)。 建議121:線程優(yōu)先級只使用三個等級; (線程優(yōu)先級推薦使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三個級別,不建議使用其他7個數(shù)字)(線程的優(yōu)先級(Priority)決定了線程獲得CPU運行的機(jī)會,優(yōu)先級越高,運行機(jī)會越大。事實:1、并不是嚴(yán)格尊重線程優(yōu)先級別來執(zhí)行的,分為10個級別;2、優(yōu)先級差別越大,運行機(jī)會差別越大;對于Java來說,JVM調(diào)用操作系統(tǒng)的接口設(shè)置優(yōu)先級,比如Windows是通過調(diào)用SetThreadPriority函數(shù)來設(shè)置的。不同操作系統(tǒng)線程優(yōu)先級設(shè)置是不相同的,Windows有7個優(yōu)先級,Linux有140個優(yōu)先級,F(xiàn)reebsd有255個優(yōu)先級。Java締造者也發(fā)現(xiàn)了該問題,于是在Thread類中設(shè)置了三個優(yōu)先級,建議使用優(yōu)先級常量,而不是1到10隨機(jī)的數(shù)字)。 建議122:使用線程異常處理器提升系統(tǒng)可靠性; (可以使用線程異常處理器來處理相關(guān)異常情況的發(fā)生,比如當(dāng)機(jī)自動重啟,大大提高系統(tǒng)的可靠性。在實際環(huán)境中應(yīng)用注意以下三點:1、共享資源鎖定;2、臟數(shù)據(jù)引起系統(tǒng)邏輯混亂;3、內(nèi)存溢出,線程異常了,但由該線程創(chuàng)建的對象并不會馬上回收,如果再重新啟動新線程,再創(chuàng)建一批新對象,特別是加入了場景接管,就危險了,有可能發(fā)生OutOfMemory內(nèi)存泄露問題)。 建議123:volatile不能保證數(shù)據(jù)同步; (volatile不能保證數(shù)據(jù)是同步的,只能保證線程能夠獲得最新值)(volatile關(guān)鍵字比較少用的原因:1、Java1.5之前該關(guān)鍵字在不同的操作系統(tǒng)上有不同的表現(xiàn),移植性差;2、比較難設(shè)計,而且誤用較多。在變量錢加上一個volatile關(guān)鍵字,可以確保每個線程對本地變量的訪問和修改都是直接與主內(nèi)存交互的,而不是與本地線程的工作內(nèi)存交互的,保證每個線程都能獲得最“新鮮”的變量值。但是volatile關(guān)鍵字并不能保證線程安全,它只能保證當(dāng)前線程需要該變量的值時能夠獲得最新的值,而不能保證多個線程修改的安全性)。 建議124:異步運算考慮使用Callable接口; (多線程應(yīng)用的兩種實現(xiàn)方式:一種是實現(xiàn)Runnable接口,另一種是繼承Thread類,這兩個方式都有缺點:run方法沒有返回值,不能拋出異常(歸根到底是Runnable接口的缺陷,Thread也是實現(xiàn)了Runnable接口),如果需要知道一個線程的運行結(jié)果就需要用戶自行設(shè)計,線程類本身也不能提供返回值和異常。Java1.5開始引入了新的接口Callable,類似于Runnable接口,實現(xiàn)它就可以實現(xiàn)多線程任務(wù),實現(xiàn)Callable接口的類,只是表明它是一個可調(diào)用的任務(wù),并不表示它具有多線程運算能力,還是需要執(zhí)行器來執(zhí)行的)。 建議125:優(yōu)先選擇線程池; (Java1.5以前,實現(xiàn)多線程比較麻煩,需要自己啟動線程,并關(guān)注同步資源,防止出現(xiàn)線程死鎖等問題,Java1.5以后引入了并行計算框架,大大簡化了多線程開發(fā)。線程有五個狀態(tài):新建狀態(tài)(New)、可運行狀態(tài)(Runnable,也叫作運行狀態(tài))、阻塞狀態(tài)(Blocked)、等待狀態(tài)(Waiting)、結(jié)束狀態(tài)(Terminated),線程的狀態(tài)只能由新建轉(zhuǎn)變?yōu)檫\行態(tài)后才可能被阻塞或等待,最后終結(jié),不可能產(chǎn)生本末倒置的情況,比如想把結(jié)束狀態(tài)變?yōu)樾陆顟B(tài),則會出現(xiàn)異常。線程運行時間分為三個部分:T1為線程啟動時間;T2為線程體運行時間;T3為線程銷毀時間。每次創(chuàng)建線程都會經(jīng)過這三個時間會大大增加系統(tǒng)的響應(yīng)時間。T2是無法避免的,只能通過優(yōu)化代碼來降低運行時間。T1和T3都可以通過線程池(Thread Pool)來縮短時間。線程池的實現(xiàn)涉及一下三個名詞:1、工作線程(Worker),線程池中的線程只有兩個狀態(tài):可運行狀態(tài)和等待狀態(tài);2、任務(wù)接口(Task),每個任務(wù)必須實現(xiàn)的接口,以供工作線程調(diào)度器調(diào)度,它主要規(guī)定了任務(wù)的入口、任務(wù)執(zhí)行完的場景處理、任務(wù)的執(zhí)行狀態(tài)等。這里的兩種類型的任務(wù):具有返回值(或異常)的Callable接口任務(wù)和無返回值并兼容舊版本的Runnable接口任務(wù);3、任務(wù)隊列(Work Queue),也叫作工作隊列,用于存放等待處理的任務(wù),一般是BlockingQueue的實現(xiàn)類,用來實現(xiàn)任務(wù)的排隊處理。線程池的創(chuàng)建過程:創(chuàng)建一個阻塞隊列以容納任務(wù),在第一次執(zhí)行任務(wù)時闖將足夠多的線程(不超過許可線程數(shù)),并處理任務(wù),之后每個工作線程自行從任務(wù)隊列中獲得任務(wù),直到任務(wù)隊列中任務(wù)數(shù)量為0為止,此時,線程將處于等待狀態(tài),一旦有任務(wù)加入到隊列中,即喚醒工作線程進(jìn)行處理,實現(xiàn)線程的可復(fù)用性)。 建議126:適時選擇不同的線程池來實現(xiàn); (Java的線程池實現(xiàn)從根本上來說只有兩個:ThreadPoolExecutor類和ScheduledThreadPoolExecutor類,還是父子關(guān)系。為了簡化并行計算,Java還提供了一個Executors的靜態(tài)類,它可以直接生成多種不同的線程池執(zhí)行器,比如單線程執(zhí)行器、帶緩沖功能的執(zhí)行器等,歸根結(jié)底還是以上兩個類的封裝類)。 建議127:Lock與synchronized是不一樣的; (Lock類(顯式鎖)和synchronized關(guān)鍵字(內(nèi)部鎖)用在代碼塊的并發(fā)性和內(nèi)存上時的語義是一樣的,都是保持代碼塊同時只有一個線程具有執(zhí)行權(quán)。顯式鎖的鎖定和釋放必須在一個try...finally塊中,這是為了確保即使出現(xiàn)運行期異常也能正常釋放鎖,保證其他線程能夠順利執(zhí)行。Lock鎖為什么不出現(xiàn)互斥情況,所有線程都是同時執(zhí)行的?原因:這是因為對于同步資源來說,顯式鎖是對象級別的鎖,而內(nèi)部鎖是類級別的鎖,也就是說Lock鎖是跟隨對象的,synchronized鎖是跟隨類的,更簡單地說把Lock定義為多線程類的私有屬性是起不到資源互斥作用的,除非是把Lock定義為所有線程共享變量。除了以上不同點之外,還有以下4點不同:1、Lock支持更細(xì)粒度的鎖控制,假設(shè)讀寫鎖分離,寫操作時不允許有讀寫操作存在,而讀操作時讀寫可以并發(fā)執(zhí)行,這一點內(nèi)部鎖很難實現(xiàn);2、Lock是無阻塞鎖,synchronized是阻塞鎖,線程A持有鎖,線程B也期望獲得鎖時,如果為Lock,則B線程為等待狀態(tài),如果為synchronized,則為阻塞狀態(tài);3、Lock可實現(xiàn)公平鎖,synchronized只能是非公平鎖,什么叫做非公平鎖?當(dāng)一個線程A持有鎖,而線程B、C處于阻塞(或等待)狀態(tài)時,若線程A釋放鎖。JVM將從線程B、C中隨機(jī)選擇一個線程持有鎖并使其獲得執(zhí)行權(quán),這叫做非公平鎖(因為它拋棄了先來后到的順序);若JVM選擇了等待時間最長的一個線程持有鎖,則為公平鎖。需要注意的是,即使是公平鎖,JVM也無法準(zhǔn)確做到“公平”,在程序中不能以此作為精確計算。顯式鎖默認(rèn)是非公平鎖,但可以在構(gòu)造函數(shù)中加入?yún)?shù)true來聲明出公平鎖;4、Lock是代碼級的,synchronized是JVM級的,Lock是通過編碼實現(xiàn)的,synchronized是在運行期由JVM解釋的,相對來說synchronized的優(yōu)化可能性更高,畢竟是在最核心不為支持的,Lock的優(yōu)化需要用戶自行考慮。相對來說,顯式鎖使用起來更加便利和強大,在實際開發(fā)中選擇哪種類型的鎖就需要根據(jù)實際情況考慮了:靈活、強大則選擇Lock,快捷、安全則選擇synchronized)。 建議128:預(yù)防線程死鎖; (線程死鎖(DeadLock)是多線程編碼中最頭疼問題,也是最難重現(xiàn)的問題,因為Java是單進(jìn)程多線程語言。要達(dá)到線程死鎖需要四個條件:1、互斥條件;2、資源獨占條件;3、不剝奪條件;4、循環(huán)等待條件;按照以下兩種方式來解決:1、避免或減少資源貢獻(xiàn);2、使用自旋鎖,如果在獲取自旋鎖時鎖已經(jīng)有保持者,那么獲取鎖操作將“自旋”在那里,直到該自旋鎖的保持者釋放了鎖為止)。 建議129:適當(dāng)設(shè)置阻塞隊列長度; (阻塞隊列BlockingQueue擴(kuò)展了Queue、Collection接口,對元素的插入和提取使用了“阻塞”處理。但是BlockingQueue不能夠自行擴(kuò)容,如果隊列已滿則會報IllegalStateException:Queue full隊列已滿異常;這是阻塞隊列和非阻塞隊列一個重要區(qū)別:阻塞隊列的容量是固定的,非阻塞隊列則是變長的。阻塞隊列可以在聲明時指定隊列的容量,若指定的容量,則元素的數(shù)量不可超過該容量,若不指定,隊列的容量為Integer的最大值。有此區(qū)別的原因是:阻塞隊列是為了容納(或排序)多線程任務(wù)而存在的,其服務(wù)的對象是多線程應(yīng)用,而非阻塞隊列容納的則是普通的數(shù)據(jù)元素。阻塞隊列的這種機(jī)制對異步計算是非常有幫助的,如果阻塞隊列已滿,再加入任務(wù)則會拒絕加入,而且返回異常,由系統(tǒng)自行處理,避免了異步計算的不可知性。可以使用put方法,它會等隊列空出元素,再讓自己加入進(jìn)去,無論等待多長時間都要把該元素插入到隊列中,但是此種等待是一個循環(huán),會不停地消耗系統(tǒng)資源,當(dāng)?shù)却尤氲脑財?shù)量較多時勢必會對系統(tǒng)性能產(chǎn)生影響。offer方法可以優(yōu)化一下put方法)。 建議130:使用CountDownLatch協(xié)調(diào)子線程; (CountDownLatch協(xié)調(diào)子線程步驟:一個開始計數(shù)器,多個結(jié)束計數(shù)器:1、每一個子線程開始運行,執(zhí)行代碼到begin.await后線程阻塞,等待begin的計數(shù)變?yōu)?;2、主線程調(diào)用begin的countDown方法,使begin的計數(shù)器為0;3、每個線程繼續(xù)運行;4、主線程繼續(xù)運行下一條語句,end的計數(shù)器不為0,主線程等待;5、每個線程運行結(jié)束時把end的計數(shù)器減1,標(biāo)志著本線程運行完畢;6、多個線程全部結(jié)束,end計數(shù)器為0;7、主線程繼續(xù)執(zhí)行,打印出結(jié)果。類似:領(lǐng)導(dǎo)安排了一個大任務(wù)給我,我一個人不可能完成,于是我把該任務(wù)分解給10個人做,在10個人全部完成后,我把這10個結(jié)果組合起來返回給領(lǐng)導(dǎo)--這就是CountDownLatch的作用)。 建議131:CyclicBarrier讓多線程齊步走; (CyclicBarrier關(guān)卡可以讓所有線程全部處于等待狀態(tài)(阻塞),然后在滿足條件的情況下繼續(xù)執(zhí)行,這就好比是一條起跑線,不管是如何到達(dá)起跑線的,只要到達(dá)這條起跑線就必須等待其他人員,待人員到齊后再各奔東西,CyclicBarrier關(guān)注的是匯合點的信息,而不在乎之前或者之后做何處理。CyclicBarrier可以用在系統(tǒng)的性能測試中,測試并發(fā)性)。 第10章 性能和效率 建議132:提升Java性能的基本方法; (如何讓Java程序跑的更快、效率更高、吞吐量更大:1、不要在循環(huán)條件中計算,每循環(huán)一次就會計算一次,會降低系統(tǒng)效率;2、盡可能把變量、方法聲明為final static類型,加上final static修飾后,在類加載后就會生成,每次方法調(diào)用則不再重新生成對象了;3、縮小變量的作用范圍,目的是加快GC的回收;4、頻繁字符串操作使用StringBuilder或StringBuffer;5、使用非線性檢索,使用binarySearch查找會比indexOf查找元素快很多,但是使用binarySearch查找時記得先排序;6、覆寫Exception的fillInStackTrace方法,fillInStackTrace方法是用來記錄異常時的棧信息的,這是非常耗時的動作,如果不需要關(guān)注棧信息,則可以覆蓋,以提升性能;7、不建立冗余對象)。 建議133:若非必要,不要克隆對象; (克隆對象并不比直接生成對象效率高)(通過clone方法生成一個對象時,就會不再執(zhí)行構(gòu)造函數(shù)了,只是在內(nèi)存中進(jìn)行數(shù)據(jù)塊的拷貝,看上去似乎應(yīng)該比new方法的性能好很多,但事實上,一般情況下new生成的對象比clone生成的性能方面要好很多。JVM對new做了大量的系能優(yōu)化,而clone方式只是一個冷僻的生成對象的方式,并不是主流,它主要用于構(gòu)造函數(shù)比較復(fù)雜,對象屬性比較多,通過new關(guān)鍵字創(chuàng)建一個對象比較耗時間的時候)。 建議134:推薦使用“望聞問切”的方式診斷性能; (性能診斷遵循“望聞問切”,不可過度急躁)。 建議135:必須定義性能衡量標(biāo)準(zhǔn); (原因:1、性能衡量標(biāo)準(zhǔn)是技術(shù)與業(yè)務(wù)之間的契約;2、性能衡量標(biāo)志是技術(shù)優(yōu)化的目標(biāo))。 建議136:槍打出頭鳥--解決首要系統(tǒng)性能問題; (解決性能優(yōu)化要“單線程”小步前進(jìn),避免關(guān)注點過多而導(dǎo)致精力分散)(解決性能問題時,不要把所有的問題都擺在眼前,這只會“擾亂”你的思維,集中精力,找到那個“出頭鳥”,解決它,在大部分情況下,一批性能問題都會迎刃而解)。 建議137:調(diào)整JVM參數(shù)以提升性能;( 四個常用的JVM優(yōu)化手段: 1、調(diào)整堆內(nèi)存大小,JVM兩種內(nèi)存:棧內(nèi)存(Stack)和堆內(nèi)存(Heap),棧內(nèi)存的特點是空間小,速度快,用來存放對象的引用及程序中的基本類型;而堆內(nèi)存的特點是空間比較大,速度慢,一般對象都會在這里生成、使用和消亡。??臻g由線程開辟,線程結(jié)束,??臻g由JVM回收,它的大小一般不會對性能有太大影響,但是它會影響系統(tǒng)的穩(wěn)定性,超過棧內(nèi)存的容量時,會拋StackOverflowError錯誤??梢酝ㄟ^“java -Xss <size>”設(shè)置棧內(nèi)存大小來解決。堆內(nèi)存的調(diào)整不能太隨意,調(diào)整得太小,會導(dǎo)致Full GC頻繁執(zhí)行,輕則導(dǎo)致系統(tǒng)性能急速下降,重則導(dǎo)致系統(tǒng)根本無法使用;調(diào)整得太大,一則浪費資源(若設(shè)置了最小堆內(nèi)存則可以避免此問題),二則是產(chǎn)生系統(tǒng)不穩(wěn)定的情況,設(shè)置方法“java -Xmx1536 -Xms1024m”,可以通過將-Xmx和-Xms參數(shù)值設(shè)置為相同的來固定堆內(nèi)存大小; 2、調(diào)整堆內(nèi)存中各分區(qū)的比例,JVM的堆內(nèi)存包括三部分:新生區(qū)(Young Generation Space)、養(yǎng)老區(qū)(Tenure Generation Space)、永久存儲區(qū)(Permanent Space 方法區(qū)),其中新生成的對象都在新生區(qū),又分為伊甸區(qū)(Eden Space)、幸存0區(qū)(Survivor 0 Space)和幸存1區(qū)(Survivor 1 Space),當(dāng)在程序中使用了new關(guān)鍵字時,首先在Eden區(qū)生成該對象,如果Eden區(qū)滿了,則觸發(fā)minor GC,然后把剩余的對象移到Survivor區(qū)(0區(qū)或者1區(qū)),如果Survivor取也滿了,則minor GC會再回收一次,然后再把剩余的對象移到養(yǎng)老區(qū),如果養(yǎng)老區(qū)也滿了,則會觸發(fā)Full GC(非常危險的動作,JVM會停止所有的執(zhí)行,所有系統(tǒng)資源都會讓位給垃圾回收器),會對所有的對象過濾一遍,檢查是否有可以回收的對象,如果還是沒有的話,就拋出OutOfMemoryError錯誤。一般情況下新生區(qū)與養(yǎng)老區(qū)的比例為1:3左右,設(shè)置命令:“java -XX:NewSize=32m -XX:MaxNewSize=640m -XX:MaxPermSize=1280m -XX:NewRatio=5”,該配置指定新生代初始化為32MB(也就是新生區(qū)最小內(nèi)存為32M),最大不超過640MB,養(yǎng)老區(qū)最大不超過1280MB,新生區(qū)和養(yǎng)老區(qū)的比例為1:5.一般情況下Eden Space : Survivor 0 Space : Survivor 1 Space == 8 : 1 : 1); 3、變更GC的垃圾回收策略,設(shè)置命令“java -XX:+UseParallelGC -XX:ParallelGCThreads=20”,這里啟用了并行垃圾收集機(jī)制,并且定義了20個收集線程(默認(rèn)的收集線程等于CPU的數(shù)量),這對多CPU的系統(tǒng)時非常有幫助的,可以大大減少垃圾回收對系統(tǒng)的影響,提高系統(tǒng)性能; 4、更換JVM,如果所有的JVM優(yōu)化都不見效,那就只有更換JVM了,比較流行的三個JVM產(chǎn)品:Java HotSpot VM、Oracle JRockit JVM、IBM JVM。 建議138:性能是個大“咕咚”; (1、沒有慢的系統(tǒng),只有不滿足義務(wù)的系統(tǒng);2、沒有慢的系統(tǒng),只有架構(gòu)不良的系統(tǒng);3、沒有慢的系統(tǒng),只有懶惰的技術(shù)人員;4、沒有慢的系統(tǒng),只有不愿意投入的系統(tǒng))。 第11章 開源世界 建議139:大膽采用開源工具; (選擇開源工具和框架時要遵循一定的規(guī)則:1、普適性原則;2、唯一性原則;3、“大樹納涼”原則;4、精而專原則;5、高熱度原則)。 建議140:推薦使用Guava擴(kuò)展工具包; (Guava(石榴)是Google發(fā)布的,其中包含了collections、caching、primitives support、concurrency libraries、common annotations、I/O等)。 建議141:Apache擴(kuò)展包; (Apache Commons通用擴(kuò)展包基本上是每個項目都會使用的,一般情況下lang包用作JDK的基礎(chǔ)語言擴(kuò)展。Apache Commons項目包含非常好用的工具,如DBCP、net、Math等)。 建議142:推薦使用Joda日期時間擴(kuò)展包; (Joda可以很好地與現(xiàn)有的日期類保持兼容,在需要復(fù)雜的日期計算時使用Joda。日期工具類也可以選擇date4j)。 建議143:可以選擇多種Collections擴(kuò)展; (三個比較有個性的Collections擴(kuò)展工具包:1、FastUtil,主要提供兩種功能:一種是限定鍵值類型的Map、List、Set等,另一種是大容量的集合;2、Trove,提供了一個快速、高效、低內(nèi)存消耗的Collection集合,并且還提供了過濾和攔截功能,同時還提供了基本類型的集合;3、lambdaj,是一個純凈的集合操作工具,它不會提供任何的集合擴(kuò)展,只會提供對集合的操作,比如查詢、過濾、統(tǒng)一初始化等)。 第12章 思想為源 建議144:提倡良好的代碼風(fēng)格; (良好的編碼風(fēng)格包括:1、整潔;2、統(tǒng)一;3、流行;4、便捷,推薦使用Checkstyle檢測代碼是否遵循規(guī)范)。 建議145:不要完全依靠單元測試來發(fā)現(xiàn)問題; (單元測試的目的是保證各個獨立分隔的程序單元的正確性,雖然它能夠發(fā)現(xiàn)程序中存在的問題(或缺陷、或錯誤),但是單元測試只是排查程序錯誤的一種方式,不能保證代碼中的所有錯誤都能被單元測試挖掘出來,原因:1、單元測試不可能測試所有的場景(路徑);2、代碼整合錯誤是不可避免的;3、部分代碼無法(或很難)測試;4、單元測試驗證的是編碼人員的假設(shè))。 建議146:讓注釋正確、清晰、簡潔; (注釋不是美化劑,而是催化劑,或為優(yōu)秀加分,或為拙略減分)。 建議147:讓接口的職責(zé)保持單一; (接口職責(zé)一定要單一,實現(xiàn)類職責(zé)盡量單一)(單一職責(zé)原則(Single Responsibility Principle,簡稱SRP)有以下三個優(yōu)點:1、類的復(fù)雜性降低;2、可讀性和可維護(hù)性提高;3、降低變更風(fēng)險)。 建議148:增強類的可替換性; (Java的三大特征:封裝、繼承、多態(tài);說說多態(tài),一個接口可以有多種實現(xiàn)方式,一個父類可以有多個子類,并且可以把不同的實現(xiàn)或子類賦給不同的接口或父類。多態(tài)的好處非常多,其中一點就是增強了類的可替換性,但是單單一個多態(tài)特性,很難保證我們的類是完全可以替換的,幸好還有一個里氏替換原則來約束。里氏替換原則:所有引用基類的地方必須能透明地使用其子類的對象。通俗點講,只要父類型能出現(xiàn)的地方子類型就可以出現(xiàn),而且將父類型替換為子類型還不會產(chǎn)生任何錯誤或異常,使用者可能根本就不需要知道是父類型還是子類型。為了增強類的可替換性,在設(shè)計類時需要考慮以下三點:1、子類型必須完全實現(xiàn)父類型的方法;2、前置條件可以被放大;3、后置條件可以被縮?。?/span> 建議149:依賴抽象而不是實現(xiàn); (此處的抽象是指物體的抽象,比如出行,依賴的是抽象的運輸能力,而不是具體的運輸交通工具。依賴倒置原則(Dependence Inversion Principle,簡稱DIP)要求實現(xiàn)解耦,保持代碼間的松耦合,提高代碼的復(fù)用率。DIP的原始定義包含三層含義:1、高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象;2、抽象不應(yīng)該依賴細(xì)節(jié);3、細(xì)節(jié)應(yīng)該依賴抽象;DIP在Java語言中的表現(xiàn)就是:1、模塊間的依賴是通過抽象發(fā)生的,實現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的;2、接口或抽象類不依賴于實現(xiàn)類;3、實現(xiàn)類依賴接口或抽象類;更加精簡的定義就是:面向接口編程。實現(xiàn)模塊間的松耦合遵循規(guī)則:1、盡量抽象;2、表面類型必須是抽象的;3、任何類都不應(yīng)該從具體類派生;4、盡量不要覆寫基類的方法;5、抽象不關(guān)注細(xì)節(jié))。 建議150:拋棄7條不良的編碼習(xí)慣; (1、自由格式的代碼;2、不使用抽象的代碼;3、彰顯個性的代碼;4、死代碼;5、冗余代碼;6、拒絕變化的代碼;7、自以為是的代碼)。 建議151:以技術(shù)人員自律而不是工人; (20條建議:1、熟悉工具;2、使用IDE;3、堅持編碼;4、編碼前思考;5、堅持重構(gòu);6、多寫文檔;7、保持程序版本的簡單性;8、做好備份;9、做單元測試;10、不要重復(fù)發(fā)明輪子;11、不要拷貝;12、讓代碼充滿靈性;13、測試自動化;14、做壓力測試;15、“剽竊”不可恥;16、堅持向敏捷學(xué)習(xí);17、重里更重面;18、分享;19、刨根問底;20、橫向擴(kuò)展)。 |
|
來自: 關(guān)平藏書 > 《JAVA》