編寫高質量代碼改善Java程序的151個建議--總結摘抄

第一章  Java開發中通用的方法和準則

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

(i、l、1;o、0等)。


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

(代碼運行工程中不要改變常量值)。


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

(不一致會導致自動類型轉換,類型提升int->float->double等)。


建議4:避免帶有變長參數的方法重載;

(變長參數的方法重載之後可能會包含原方法)。


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

(兩個都包含變長參數的重載方法,當變長參數部分空值,或者爲null值時,重載方法不清楚會調用哪一個方法)。


建議6:覆寫變長方法也循規蹈矩;

(變長參數與數組,覆寫的方法參數與父類相同,不僅僅是類型、數量,還包括顯示形式)。


建議7:警惕自增的陷阱;

(count=count++;操作時JVM首先將count原值拷貝到臨時變量區,再執行count加1,之後再將臨時變量區的值賦給count,所以count一直爲0或者某個初始值。C++中count=count++;與count++等效,而PHP與Java類似)。


建議8:不要讓舊語法困擾你;

(Java中拋棄了C語言中的goto語法,但是還保留了該關鍵字,只是不進行語義處理,const關鍵中同樣類似)。


建議9:少用靜態導入;

(Java5引入的靜態導入語法import static,使用靜態導入可以減少程序字符輸入量,但是會帶來很多代碼歧義,省略的類約束太少,顯得程序晦澀難懂)。


建議10:不要在本類中覆蓋靜態導入的變量和方法;

(例如靜態導入Math包下的PI常量,類屬性中又定義了一個同樣名字PI的常量。編譯器的“最短路徑原則”將會選擇使用本類中的PI常量。本類中的屬性,方法優先。如果要變更一個被靜態導入的方法,最好的辦法是在原始類中重構,而不是在本類中覆蓋)。


建議11:養成良好的習慣,顯式聲明UID;

(顯式聲明serialVersionUID可以避免序列化和反序列化中對象不一致,JVM根據serialVersionUID來判斷類是否發生改變。隱式聲明由編譯器在編譯的時候根據包名、類名、繼承關係等諸多因子計算得出,極其複雜,算出的值基本唯一)。


建議12:避免用序列化類在構造函數中爲不變量賦值;

(在序列化類中,不適用構造函數爲final變量賦值)(序列化規則1:如果final屬性是一個直接量,在反序列化時就會重新計算;序列化規則2:反序列化時構造函數不會執行;反序列化執行過程:JVM從數據流中獲取一個Object對象,然後根據數據流中的類文件描述信息(在序列化時,保存到磁盤的對象文件中包含了類描述信息,不是類)查看,發現是final變量,需要重新計算,於是引用Person類中的name值,而此時JVM又發現name沒有賦值(因爲反序列化時構造函數不會執行),不能引用,於是它不再初始化,保持原始值狀態。整個過程中需要保持serialVersionUID相同)。


建議13:避免爲final變量複雜賦值;

(類序列化保存到磁盤上(或網絡傳輸)的對象文件包括兩部分:1、類描述信息:包括包路徑、繼承關係等。注意,它並不是class文件的翻版,不記錄方法、構造函數、static變量等的具體實現。2、非瞬態(transient關鍵字)和非靜態(static關鍵字)的實例變量值。總結:反序列化時final變量在以下情況下不會被重新賦值:1、通過構造函數爲final變量賦值;2、通過方法返回值爲final變量賦值;3、final修飾的屬性不是基本類型)。


建議14:使用序列化類的私有方法巧妙解決“部分屬性持久化問題”;

(部分屬性持久化問題解決方案1:把不需要持久化的屬性加上瞬態關鍵字(transient關鍵字)即可,但是會使該類失去了分佈式部署的功能。方案2:新增業務對象。方案3:請求端過濾。方案4:變更傳輸契約,即覆寫writeObject和readObject私有方法,在兩個私有方法體內完成部分屬性持久化)。


建議15:break萬萬不可忘;

(switch語句中,每一個case匹配完都需要使用break關鍵字跳出,否則會依次執行完所有的case內容。)。


建議16:易變業務使用腳本語言編寫;

(腳本語言:都是在運行期解釋執行。腳本語言三大特性:1、靈活:動態類型;2、便捷:解釋型語言,不需要編譯成二進制,不需要像Java一樣生成字節碼,依靠解釋執行,做到不停止應用變更代碼;3、簡單:部分簡單。Java使用ScriptEngine執行引擎來執行JavaScript腳本代碼)。


建議17:慎用動態編譯;

好處:更加自如地控制編譯過程。很少使用,原因:靜態編譯能夠完成大部分工作甚至全部,即使需要使用,也有很好的替代方案,如JRuby、Groovy等無縫的腳本語言。動態編譯註意以下4點:1、在框架中謹慎使用:debug困難,成本大;2、不要在要求高性能的項目中使用:需要一個編譯過程,比靜態編譯多了一個執行環節;3、動態編譯要考慮安全問題:防止惡意代碼;4、記錄動態編譯過程)。


建議18:避免instanceof非預期結果;

(instanceof用來判斷一個對象是否是一個類的實例,只能用於對象的判斷,不能用於基本類型的判斷(編譯不通過),instanceof操作符的左右操作數必須有繼承或實現關係,否則編譯會失敗。例:null instanceof String返回值是false,instanceof特有規則,若左操作數是null,結果就直接返回false,不再運算右操作數是什麼類)。


建議19:斷言絕對不是雞肋;

(防禦式編程中經常使用斷言(Assertion)對參數和環境做出判斷。斷言是爲調試程序服務的。兩個特性:1、默認assert不啓用;2、assert拋出的異常AssertionError是繼承自Error的)。


建議20:不要只替換一個類;

(發佈應用系統時禁止使用類文件替換方式,整體WAR包發佈纔是完全之策)(Client類中調用了Constant類中的屬性值,如果更改了Constant常量類屬性的值,重新編譯替換。而不改變或者替換Client類,則Client中調用的Constant常量類的屬性值並不會改變。原因:對於final修飾的基本類型和String類型,編譯器會認爲它是穩定態(Immutable Status),所以在編譯時就直接把值編譯到字節碼中了,避免了再運行期引用,以提高代碼的執行效率。而對於final修飾的類(即非基本類型),編譯器認爲它是不穩定態(Mutable Status),在編譯時建立的則是引用關係(該類型也叫作Soft Final),如果Client類引入的常量是一個類或實例,即使不重新編譯也會輸出最新值)。


第二章  基本類型

建議21:用偶判斷,不用奇判斷;

(不要使用奇判斷(i%2 == 1 ? "奇數" : "偶數"),使用偶判斷(i%2 == 0 ? "偶數" : "奇數")。原因Java中的取餘(%標識符)算法:測試數據輸入1 2 0 -1 -2,奇判斷的時候,當輸入-1時,也會返回偶數。

//模擬取餘計算,dividend被除數,divisor除數
public static int remainder(int dividend, int divisor) {
	return dividend - dividend / divisor * divisor;
}
)。


建議22:用整數類型處理貨幣;

(不要使用float或者double計算貨幣,因爲在計算機中浮點數“有可能”是不準確的,它只能無限接近準確值,而不能完全精確。不能使用計算機中的二進制位來表示如0.4等的浮點數。解決方案:1、使用BigDecimal(優先使用);2、使用整型)。


建議23:不要讓類型默默轉換;

(基本類型轉換時,使用主動聲明方式減少不必要的Bug)

public static final int LIGHT_SPEED = 30 * 10000 * 1000;
long dis2 = LIGHT_SPEED * 60 * 8;

以上兩句在參與運算時會溢出,因爲Java是先運算後再進行類型轉換的。因爲dis2的三個運算參數都是int類型,三者相乘的結果也是int類型,但是已經超過了int的最大值,所以越界了。解決方法,在運算參數60後加L即可。


建議24:邊界、邊界、還是邊界;

(數字越界是檢驗條件失效,邊界測試;檢驗條件if(order>0 && order+cur<=LIMIT),輸入的數大於0,加上cur的值之後溢出爲負值,小於LIMIT,所以滿足條件,但不符合要求)。


建議25:不要讓四捨五入虧了一方;

(Math.round(10.5)輸出結果11;Math.round(-10.5)輸出結果-10。這是因爲Math.round採用的舍入規則所決定的(採用的是正無窮方向舍入規則),根據不同的場景,慎重選擇不同的舍入模式,以提高項目的精準度,減少算法損失)。


建議26:提防包裝類型的null值;

(泛型中不能使用基本類型,只能使用包裝類型,null執行自動拆箱操作會拋NullPointerException異常,因爲自動拆箱是通過調用包裝對象的intValue方法來實現的,而訪問null的intValue方法會報空指針異常。謹記一點:包裝類參與運算時,要做null值校驗,即(i!=null ? i : 0))。


建議27:謹慎包裝類型的大小比較;

(大於>或者小於<比較時,包裝類型會調用intValue方法,執行自動拆箱比較。而==等號用來判斷兩個操作數是否有相等關係的,如果是基本類型則判斷數值是否相等,如果是對象則判斷是否是一個對象的兩個引用,也就是地址是否相等。通過兩次new操作產生的兩個包裝類型,地址肯定不相等)。


建議28:優先使用整型池;

自動裝箱是通過調用valueOf方法來實現的,包裝類的valueOf生成包裝實例可以顯著提高空間和時間性能)valueOf方法實現源碼:

public static Integer valueOf(int i) {
	final int offset = 128;
	if (i >= -128 && i <=127) {
		return IntegerCache.cache[i + offset];
	}
	return new Integer(i);
}
class IntegerCache {
	static final Integer cache[] = new Integer[-(-128) + 127 + 1];
	static {
		for (int i = 0; i < cache.length; i++) 
			cache[i] = new Integer(i - 128);
	}
}

cache是IntegerCache內部類的一個靜態數組,容納的是-128到127之間的Integer對象。通過valueOf產生包裝對象時,如果int參數在-128到127之間,則直接從整型池中獲得對象,不在該範圍的int類型則通過new生成包裝對象。在判斷對象是否相等的時候,最好是利用equals方法,避免“==”產生非預期結果。


建議29:優先選擇基本類型;

(int參數先加寬轉變成long型,然後自動轉換成Long型。Integer.valueOf(i)參數先自動拆箱轉變爲int類型,與之前類似)。


建議30:不要隨便設置隨機種子;

(若非必要,不要設置隨機數種子)(Random r = new Random(1000);該代碼中1000即爲隨機種子。在同一臺機器上,不管運行多少次,所打印的隨機數都是相同的。在Java中,隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個規則:1、種子不同,產生不同的隨機數;2、種子相同,即使實例不同也產生相同的隨機數。Random類默認種子(無參構造)是System.nanoTime()的返回值,這個值是距離某一個固定時間點的納秒數,所以可以產生隨機數。java.util.Random類與Math.random方法原理相同)。


第三章  類、對象及方法

建議31:在接口中不要存在實現代碼;

(可以通過在接口中聲明一個靜態常量s,其值是一個匿名內部類的實例對象,可以實現接口中存在實現代碼)。


建議32:靜態變量一定要先聲明後賦值;

(也可以先使用後聲明,因爲靜態變量是類初始化時首先被加載,JVM會去查找類中所有的靜態聲明,然後分配空間,分配到數據區(Data Area)的,它在內存中只有一個拷貝,不會被分配多次,注意這時候只是完成了地址空間的分配還沒有賦值,之後JVM會根據類中靜態賦值(包括靜態類賦值和靜態塊賦值)的先後順序來執行,後面的操作都是地址不變,值改變)。


建議33:不要覆寫靜態方法;

(一個實例對象有兩個類型:表面類型實際類型,表面類型是聲明時的類型,實際類型是對象產生時的類型。對於非靜態方法,它是根據對象的實際類型來執行的,即執行了覆寫方法。而對於靜態方法,首先靜態方法不依賴實例對象,通過類名訪問;其次,可以通過對象訪問靜態方法,如果通過對象訪問,JVM則會通過對象的表面類型查找到靜態方法的入口,繼而執行)。


建議34:構造函數儘量簡化;

(通過new關鍵字生成對象時必然會調用構造函數。子類實例化時,首先會初始化父類(注意這裏是初始化,可不是生成父類對象),也就是初始化父類的變量,調用父類的構造函數,然後纔會初始化子類的變量,調用子類自己的構造函數,最後生成一個實例對象。構造函數太複雜有可能造成,對象使用時還沒完成初始化)。


建議35:避免在構造函數中初始化其他類;

(有可能造成不斷的new新對象的死循環,直到棧內存被消耗完拋出StackOverflowError異常爲止)。


建議36:使用構造代碼塊精煉程序;

(四種類型的代碼塊:1、普通代碼塊:在方法後面使用“{}”括起來的代碼片段;2、靜態代碼塊:在類中使用static修飾,並使用“{}”括起來的代碼片段;3、同步代碼塊:使用synchronized關鍵字修飾,並使用“{}”括起來的代碼片段,表示同一時間只能有一個縣城進入到該方法;4、構造代碼塊:在類中沒有任何的前綴或後綴,並使用“{}”括起來的代碼片段。編譯器會把構造代碼塊插入到每個構造函數的最前端。構造代碼塊的兩個特性:1、在每個構造函數中都運行;2、在構造函數中它會首先運行)。


建議37:構造代碼塊會想你所想;

(編譯器會把構造代碼塊插入到每一個構造函數中,有一個特殊情況:如果遇到this關鍵字(也就是構造函數調用自身其他的構造函數時)則不插入構造代碼塊。如果遇到super關鍵字,編譯器會把構造代碼塊插入到super方法之後執行)。


建議38:使用靜態內部類提高封裝性;

(Java嵌套內分爲兩種:1、靜態內部類2、內部類;靜態內部類兩個優點:加強了類的封裝性和提高了代碼的可讀性。靜態內部類與普通內部類的區別1、靜態內部類不持有外部類的引用,在普通內部類中,我們可以直接訪問外部類的屬性、方法,即使是private類型也可以訪問,這是因爲內部類持有一個外部類的引用,可以自由訪問。而靜態內部類,則只可以訪問外部類的靜態方法和靜態屬性,其他則不能訪問。2、靜態內部類不依賴外部類,普通內部類與外部類之間是相互依賴的關係,內部類不能脫離外部類實例,同聲同死,一起聲明,一起被垃圾回收器回收。而靜態內部類可以獨立存在,即使外部類消亡了;3、普通內部類不能聲明static的方法和變量,注意這裏說的是變量,常量(也就是final static修飾的屬性)還是可以的,而靜態內部類形似外部類,沒有任何限制)。


建議39:使用匿名類的構造函數;

List l2 = new ArrayList(){}; //定義了一個繼承於ArrayList的匿名類,只是沒有任何的覆寫方法而已

List l3 = new ArrayList(){{}}; //定義了一個繼承於ArrayList的匿名類,並且包含一個初始化塊,類似於構造代碼塊)<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">)</span>


建議40:匿名類的構造函數很特殊;

(匿名類初始化時直接調用了父類的同參數構造器,然後再調用自己的構造代碼塊)


建議41:讓多重繼承成爲現實;

(Java中一個類可以多種實現,但不能多重繼承。使用成員內部類實現多重繼承。內部類一個重要特性:內部類可以繼承一個與外部類無關的類,保證了內部類的獨立性,正是基於這一點,多重繼承纔會成爲可能)。


建議42:讓工具類不可實例化;

(工具類的方法和屬性都是靜態的,不需要實例即可訪問。實現方式:將構造函數設置爲private,並且在構造函數中拋出Error錯誤異常)。


建議43:避免對象的淺拷貝;

(淺拷貝存在對象屬性拷貝不徹底的問題。對於只包含基本數據類型的類可以使用淺拷貝;而包含有對象變量的類需要使用序列化與反序列化機制實現深拷貝)。


建議44:推薦使用序列化實現對象的拷貝;

(通過序列化方式來處理,在內存中通過字節流的拷貝來實現深拷貝。使用此方法進行對象拷貝時需注意兩點:1、對象的內部屬性都是可序列化的;2、注意方法和屬性的特殊修飾符,比如final、static、transient變量的序列化問題都會影響拷貝效果。一個簡單辦法,使用Apache下的commons工具包中的SerializationUtils類,直接使用更加簡潔方便)。


建議45:覆寫equals方法時不要識別不出自己;

(需要滿足p.equals(p)放回爲真,自反性)。


建議46:equals應該考慮null值情景;

(覆寫equals方法時需要判一下null,否則可能產生NullPointerException異常)。


建議47:在equals中使用getClass進行類型判斷;

(使用getClass方法來代替instanceof進行類型判斷)。


建議48:覆寫equals方法必須覆寫hashCode方法;

(需要兩個相同對象的hashCode方法返回值相同,所以需要覆寫hashCode方法,如果不覆寫,兩個不同對象的hashCode肯定不一樣,簡單實現hashCode方法,調用org.apache.commons.lang.builder包下的Hash碼生成工具HashCodeBuilder)。


建議49:推薦覆寫toString方法;

(原始toString方法顯示不人性化)。


建議50:使用package-info類爲包服務;

(package-info類是專門爲本包服務的,是一個特殊性主要體現在3個方面:1、它不能隨便被創建;2、它服務的對象很特殊;3、package-info類不能有實現代碼;package-info類的作用:1、聲明友好類和包內訪問常量;2、爲在包上標註註解提供便利;3、提供包的整體註釋說明)。


建議51:不要主動進行垃圾回收

(主動進行垃圾回收是一個非常危險的動作,因爲System.gc要停止所有的響應(Stop 天河world),才能檢查內存中是否有可回收的對象,所有的請求都會暫停)。


第四章  字符串

建議52:推薦使用String直接量賦值;

(一般對象都是通過new關鍵字生成,String還有第二種生成方式,即直接聲明方式,如String str = "a";String中極力推薦使用直接聲明的方式,不建議使用new String("a")的方式賦值。原因:直接聲明方式:創建一個字符串對象時,首先檢查字符串常量池中是否有字面值相等的字符串,如果有,則不再創建,直接返回池中引用,若沒有則創建,然後放到池中,並返回新建對象的引用。使用new String()方式:直接聲明一個String對象是不檢查字符串常量池的,也不會吧對象放到池中。String的intern方法會檢查當前的對象在對象池中是否有字面值相同的引用對象,有則返回池中對象,沒有則放置到對象池中,並返回當前對象)。


建議53:注意方法中傳遞的參數要求;

(replaceAll方法傳遞的第一個參數是正則表達式)。


建議54:正確使用String、StringBuffer、StringBuilder;

(String使用“+”進行字符串連接,之前連接之後會產生一個新的對象,所以會不斷的創建新對象,優化之後與StringBuilder和StringBuffer採用同樣的append方法進行連接,但是每一次字符串拼接都會調用一次toString方法,所以會很耗時。StringBuffer與StringBuilder基本相同,只是一個字符數組的在擴容而已,都是可變字符序列,不同點是:StringBuffer是線程安全的,StringBuilder是線程不安全的)。


建議55:注意字符串的位置;

(在“+”表達式中,String字符串具有最高優先級)(Java對加號“+”的處理機制:在使用加號進行計算的表達式中,只要遇到String字符串,則所有的數據都會轉換爲String類型進行拼接,如果是原始數據,則直接拼接,如果是對象,則調用toString方法的返回值然後拼接)。


建議56:自由選擇字符串拼接方式;

(字符串拼接有三種方法:加號、concat方法及StringBuilder(或StringBuffer)的append方法。字符串拼接性能中,StringBuilder的append方法最快,concat方法次之,加號最慢。原因:1、“+”方法拼接字符串:雖然編譯器對字符串的加號做了優化,使用StringBuidler的append方法進行追加,但是與純粹使用StringBuilder的append方法不同:一是每次循環都會創建一個StringBuilder對象,二是每次執行完都要調用toString方法將其準換爲字符串--toString方法最耗時;2、concat方法拼接字符串:就是一個數組拷貝,但是每次的concat操作都會新創建一個String對象,這就是concat速度慢下來的真正原因;3、append方法拼接字符串:StringBuidler的append方法直接由父類AbstractStringBuilder實現,整個append方法都在做字符數組處理,沒有新建任何對象,所以速度快)。


建議57:推薦在複雜字符串操作中使用正則表達式;

(正則表達式是惡魔,威力強大,但難以控制)。


建議58:強烈建議使用UTF編碼;

(一個系統使用統一的編碼)。


建議59:對字符串排序持一種寬容的心態;

(如果排序不是一個關鍵算法,使用Collator類即可。主要針對於中文)。


第五章  數組和集合

建議60:性能考慮,數組是首選;

(性能要求較高的場景中使用數組替代集合)(基本類型在棧內存中操作,對象在堆內存中操作。數組中使用基本類型是效率最高的,使用集合類會伴隨着自動裝箱與自動拆箱動作,所以性能相對差一些)。


建議61:若有必要,使用變長數組;

使用Arrays.copyOf(datas,newLen)對原數組datas進行擴容處理)。


建議62:警惕數組的淺拷貝;

(通過Arrays.copyOf(box1,box1.length)方法產生的數組是一個淺拷貝,這與序列化的淺拷貝完全相同:基本類型是直接拷貝值,其他都是拷貝引用地址。數組中的元素沒有實現Serializable接口)。


建議63:在明確的場景下,爲集合指定初始容量;

(ArrayList集合底層使用數組存儲,如果沒有初始爲ArrayList指定數組大小,默認存儲數組大小長度爲10,添加的元素達到數組臨界值後,使用Arrays.copyOf方法進行1.5倍擴容處理。HashMap是按照倍數擴容的,Stack繼承自Vector,所採用擴容規則的也是翻倍)。


建議64:多種最值算法,適時選擇;

(最值計算時使用集合最簡單,使用數組性能最優,利用Set集合去重,使用TreeSet集合自動排序)。


建議65:避開基本類型數組轉換列表陷阱;

(原始類型數組不能作爲asList的輸入參數,否則會引起程序邏輯混亂)(基本類型是不能泛化的,在java中數組是一個對象,它是可以泛化的。使用Arrays.asList(data)方法傳入一個基本類型數組時,會將整個基本類型數組作爲一個數組對象存入,所以存入的只會是一個對象。JVM不可能輸出Array類型,因爲Array是屬於java.lang.reflect包的,它是通過反射訪問數組元素的工具類。在Java中任何一個數組的類都是“[I”,因爲Java並沒有定義數組這個類,它是編譯器編譯的時候生成的,是一個特殊的類)。


建議66:asList方法產生的List對象不可更改;

(使用add方法向asList方法生成的集合中添加元素時,會拋UnsupportedOperationException異常。原因:asList生成的ArrayList集合並不是java.util.ArrayList集合,而是Arrays工具類的一個內置類,我們經常使用的List.add和List.remove方法它都沒有實現,也就是說asList返回的是一個長度不可變的列表。此處的列表只是數組的一個外殼,不再保持列表動態變長的特性)。


建議67:不同的列表選擇不同的遍歷方法;

(ArrayList數組實現了RandomAccess接口(隨機存取接口),ArrayList是一個可以隨機存取的列表。集合底層如果是基於數組實現的,實現了RandomAccess接口的集合,使用下標進行遍歷訪問性能會更高;底層使用雙向鏈表實現的集合,使用foreach的迭代器遍歷性能會更高)。


建議68:頻繁插入和刪除時使用LinkedList;

(ArrayList集合,每次插入或者刪除一個元素,其後的所有元素就會向後或者向前移動一位,性能很低。LinkedList集合插入時不需要移動其他元素,性能高;修改元素,LinkedList集合比ArrayList集合要慢很多;添加元素,LinkedList與ArrayList集合性能差不多,LinkedList添加一個ListNode,而ArrayList則在數組後面添加一個Entry)。


建議69:列表相等只需關心元素數據;

(判斷集合是否相等時只須關注元素是否相等即可)(ArrayList與Vector都是List,都實現了List接口,也都繼承了AbstractList抽象類,其equals方法是在AbstractList中定義的。所以只要求兩個集合類實現了List接口就成,不關心List的具體實現類,只要所有的元素相等,並且長度也相等就表明兩個List是相等的,與具體的容量類型無關)。


建議70:子列表只是原列表的一個視圖;

(使用==判斷相等時,需要滿足兩個對象地址相等,而使用equals判斷兩個對象是否相等時,只需要關注表面值是否相等。subList方法是由AbstractList實現的,它會根據是不是可以隨機存取來提供不同的SubList實現方式,RandomAccessSubList是SubList子類,SubList類中subList方法的實現原理:它返回的SubList類是AbstractList的子類,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身並沒有生成一個數組或是鏈表,也就是子列表只是原列表的一個視圖,所有的修改動作都反映在了原列表上)。


建議71:推薦使用subList處理局部列表;

(需求:要刪除一個ArrayList中的20-30範圍內的元素;將原列表轉換爲一個可變列表,然後使用subList獲取到原列表20到30範圍內的一個視圖(View),然後清空該視圖內的元素,即可在原列表中刪除20到30範圍內的元素)。


建議72:生成子列表後不要再操作原列表;

(subList生成子列表後,使用Collections.unmodifiableList(list);保持原列表的只讀狀態)(利用subList生成子列表後,更改原列表,會造成子列表拋出java.util.ConcurrentModificationException異常。原因:subList取出的列表是原列表的一個視圖,原數據集(代碼中的list變量)修改了,但是subList取出的子列表不會重新生成一個新列表(這點與數據庫視圖是不相同的),後面再對子列表操作時,就會檢測到修改計數器與預期的不相同,於是就拋出了併發修改異常)。


建議73:使用Comparator進行排序;

(Comparable接口可以作爲實現類的默認排序法,Comparator接口則是一個類的擴展排序工具)(兩種數據排序實現方式:1、實現Comparable接口,必須要實現compareTo方法,一般由類直接實現,表明自身是可比較的,有了比較才能進行排序;2、實現Comparator接口,必須實現compare方法,Comparator接口是一個工具類接口:用作比較,它與原有類的邏輯沒有關係,只是實現兩個類的比較邏輯)。


建議74:不推薦使用binarySearch對列表進行檢索;

(indexOf與binarySearch方法功能類似,只是使用了二分法搜索。使用二分查找的首要條件是必須要先排序,不然二分查找的值是不準確的。indexOf方法直接就是遍歷搜尋。從性能方面考慮,binarySearch是最好的選擇)。


建議75:集合中的元素必須做到compareTo和equals同步;

(實現了compareTo方法,就應該覆寫equals方法,確保兩者同步)(在集合中indexOf方法是通過equals方法的返回值判斷的,而binarySearch查找的依據是compareTo方法的返回值;equals是判斷元素是否相等,compareTo是判斷元素在排序中的位置是否相同)。


建議76:集合運算時使用更優雅的方式;

(1、並集:list1.addAll(list2); 2、交集:list1.retainAll(list2); 3、差集:list1.removeAll(list2); 4、無重複的並集:list2.removeAll(list1);list1.addAll(list2);)。


建議77:使用shuffle打亂列表;

(使用Collections.shuffle(tagClouds)打亂列表)。


建議78:減少HashMap中元素的數量;

(儘量讓HashMap中的元素少量並簡單)(現象:使用HashMap存儲數據時,還有空閒內存,卻拋出了內存溢出異常;原因:HashMap底層的數組變量名叫table,它是Entry類型的數組,保存的是一個一個的鍵值對。與ArrayList集合相比,HashMap比ArrayList多了一次封裝,把String類型的鍵值對轉換成Entry對象後再放入數組,這就多了40萬個對象,這是問題產生的第一個原因;HashMap在插入鍵值對時,會做長度校驗,如果大於或等於閾值(threshold變量),則數組長度增大一倍。默認閾值是當前長度與加載因子的乘積,默認的加載因子(loadFactor變量)是0.75,也就是說只要HashMap的size大於數組長度的0.75倍時,就開始擴容。導致到最後,空閒的內存空間不足以增加一次擴容時就會拋出OutOfMemoryError異常)。


建議79:集合中的哈希碼不要重複;

(列表查找不管是遍歷查找、鏈表查找或者是二分查找都不夠快。最快的是Hash開頭的集合(如HashMap、HashSet等類)查找,原理:根據hashCode定位元素在數組中的位置。HashMap的table數組存儲元素特點:1、table數組的長度永遠是2的N次冪;2、table數組中的元素是Entry類型;3、table數組中的元素位置是不連續的;每個Entry都有一個next變量,它會指向下一個鍵值對,用來鏈表的方式來處理Hash衝突的問題。如果Hash碼相同,則添加的元素都使用鏈表處理,在查找的時候這部分的性能與ArrayList性能差不多)。


建議80:多線程使用Vector或HashTable;

(Vector與ArrayList原理類似,只是是線程安全的,HashTable是HashMap的多線程版本。線程安全:基本所有的集合類都有一個叫快速失敗(Fail-Fast)的校驗機制,當一個集合在被多個線程修改並訪問時,就可能出現ConcurrentModificationException異常,這是爲了確保集合方法一致而設置的保護措施;實現原理是modCount修改計數器:如果在讀列表時,modCount發生變化(也就是有其他線程修改)則會拋出ConcurrentModificationException異常。線程同步:是爲了保護集合中的數據不被髒讀、髒寫而設置的)。


建議81:非穩定排序推薦使用List;

非穩定的意思是:經常需要改動;TreeSet集合中元素不可重複,且默認按照升序排序,是根據Comparable接口的compareTo方法的返回值確定排序位置的。SortedSet接口(TreeSet實現了該接口)只是定義了在該集合加入元素時將其進行排序,並不能保證元素修改後的排序結果。因此TreeSet適用於不變量的集合數據排序,但不適合可變量的排序。對於可變量的集合,需要自己手動進行再排序)(SortedSet中的元素被修改後可能會影響其排序位置)。


建議82:由點及面,一頁知秋--集合大家族;

1、List:實現List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一個動態數組,LinkedList是一個雙向鏈表,Vector是一個線程安全的動態數組,Stack是一個對象棧,遵循先進後出的原則;

2、Set:Set是不包含重複元素的集合,其主要的實現類有:EnumSet、HashSet、TreeSet,其中EnumSet是枚舉類型的專用Set,HashSet是以哈希碼決定其元素位置的Set,原理與HashMap相似,提供快速插入與查找方法,TreeSet是一個自動排序的Set,它實現了SortedSet接口;

3、Map:可以分爲排序Map和非排序Map;排序Map爲TreeMap,根據Key值進行自動排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子類,EnumMap則要求其Key必須是某一個枚舉類型;

4:Queue:分爲兩類,一類是阻塞式隊列,隊列滿了以後再插入元素會拋異常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是以數組方式實現的有界阻塞隊列;PriorityBlockingQueue是依照優先級組件的隊列;LinkedBlockingQueue是通過鏈表實現的阻塞隊列;另一類是非阻塞隊列,無邊界的,只要內存允許,都可以追加元素,經常使用的是PriorityQueue類。還有一種是雙端隊列,支持在頭、尾兩端插入和移除元素,主要實現類是:ArrayDeque、LinkedBlockingDeque、LinkedList;

5、數組:數組能存儲基本類型,而集合不行;所有的集合底層存儲的都是數組;

6、工具類:數組的工具類是:java.util.Arrays和java.lang.reflect.array;集合的工具類是java.util.Collections;

7、擴展類:可以使用Apache的commons-collections擴展包,也可以使用Google的google-collections擴展包)。


第六章  枚舉和註解

建議83:推薦使用枚舉定義常量;

(在項目開發中,推薦使用枚舉常量替代接口常量和類常量)(常量分爲:類常量、接口常量、枚舉常量;枚舉常量優點:1、枚舉常量更簡單;2、枚舉常量屬於穩態性(不允許發生越界);3、枚舉具有內置方法,values方法可以獲取到所有枚舉值;4、枚舉可以自定義方法)。


建議84:使用構造函數協助描述枚舉項;

(每個枚舉項都是該枚舉的一個實例。可以通過添加屬性,然後通過構造函數給枚舉項添加更多描述信息)。


建議85:小心switch帶來的空值異常;

(使用枚舉值作爲switch(枚舉類);語句的條件值時,需要對枚舉類進行判斷是否爲null值。因爲Java中的switch語句只能判斷byte、short、char、int類型,JDK7可以判斷String類型,使用switch語句判斷枚舉類型時,會根據枚舉的排序值匹配。如果傳入的只是null的話,獲取排序值需要調用如season.ordinal()方法時會拋出NullPointerException異常)。


建議86:在switch的default代碼塊中增加AssertionError錯誤;

(switch語句在使用枚舉類作爲判斷條件時,避免出現增加了一個枚舉項,而switch語句沒做任何修改,編譯不會出現問題,但是在運行期會發生非預期的錯誤。爲避免這種情況出現,建議在default後直接拋出一個AssertionError錯誤。含義是:不要跑到這裏來,一跑到這裏來馬上就會報錯)。


建議87:使用valueOf前必須進行校驗;

(Enum.valueOf()方法會把一個String類型的名稱轉變爲枚舉項,也就是在枚舉項中查找出字面值與該參數相等的枚舉項。valueOf方法先通過反射從枚舉類的常量聲明中查找,若找到就直接返回,若找不到就拋出IllegalArgumentException異常)。


建議88:用枚舉實現工廠方法模式更簡潔;

(工廠方法模式是“創建對象的接口,讓子類決定實例化哪一個類,並使一個類的實例化延遲到其子類”。枚舉實現工廠方法模式有兩種方法:1、枚舉非靜態方法實現工廠方法模式;2、通過抽象方法生成產品;優點:1、避免錯誤調用的發生;2、性能好,使用便捷;3、減低類間耦合性)。


建議89:枚舉項的數量控制在64個以內;

(Java提供了兩個枚舉集合:EnumSet、EnumMap;EnumSet要求其元素必須是某一枚舉的枚舉項,EnumMap表示Key值必須是某一枚舉的枚舉項。由於枚舉類型的實例數量固定並且有限,相對來說EnumSet和EnumMap的效率會比其他Set和Map要高。Java處理EnumSet過程:當枚舉項小於等於64時,創建一個RegularEnumSet實例對象,大於64時創一個JumboEnumSet實例對象。RegularEnumSet是把每個枚舉項編碼映射到一個long類型數字得每一位上,而JumboEnumSet則會先按照64個一組進行拆分,然後每個組再映射到一個long類型的數字得每一位上)。


建議90:小心註解繼承;

(不常用的元註解(Meta-Annotation):@Inherited,它表示一個註解是否可以自動被繼承)。


建議91:枚舉和註解結合使用威力更大;

(註解和接口寫法類似,都採用了關鍵字interface,而且都不能有實現代碼,常量定義默認都是public static final類型的等,他們的主要不同點:註解要在interface前加上@字符,而且不能繼承,不能實現)。


建議92:注意@Override不同版本的區別;

(@Override註解用於方法的覆寫上,它在編譯期有效,也就是Java編譯器在編譯時會根據該註解檢查方法是否真的是覆寫,如果不是就報錯,拒絕編譯。Java1.5版本中@Override是嚴格遵守覆寫的定義:子類方法與父類方法必須具有相同的方法名、輸入參數、輸出參數(允許子類縮小)、訪問權限(允許子類擴大),父類必須是一個類,不是是接口,否則不能算是覆寫。而在Java1.6就開放了很多,實現接口的方法也可以加上@Override註解了。如果是Java1.6版本移植到Java1.5版本中時,需要刪除接口實現方法上的@Override註解)。


第七章  泛型和反射

建議93:Java的泛型是類型擦除的;

(加入泛型優點:加強了參數類型的安全性,減少了類型的轉換。Java的泛型在編譯期有效,在運行期被刪除,也就是說所有的泛型參數類型在編譯後都會被清除掉。所以:1、泛型的class對象時是相同的;2、泛型數組初始化時不能聲明泛型類型;3、instanceof不允許存在泛型參數)。


建議94:不能初始化泛型參數和數組;

(泛型類型在編譯期被擦除,在類初始化時將無法獲得泛型的具體參數,所以泛型參數和數組無法初始化,但是ArrayList卻可以,因爲ArrayList初始化是向上轉型變成了Object類型;需要泛型數組解決辦法:只聲明,不再初始化,由構造函數完成初始化操作)。


建議95:強制聲明泛型的實際類型;

(無法從代碼中推斷出泛型類型的情況下,即可強制聲明泛型類型;方法:List<Integer> list2 = ArrayUtils.<Integer>asList();在輸入前定義這是一個Integer類型的參數)。


建議96:不同的場景使用不同的泛型通配符;

(Java泛型支持通配符(Wildcard),可以單獨使用一個“?”表示任意類,也可以使用extends關鍵字表示某一個類(接口)的子類型,還可以使用super關鍵字表示某一個類(接口)的父類型。1、泛型結構只參與“讀”操作則限定上界(extends關鍵字);2、泛型結構只參與“寫”操作則限定下界(使用super關鍵字);3、如果一個泛型結構既用作“讀”操作也用作“寫”操作則使用確定的泛型類型即可,如List<E>)。


建議97:警惕泛型是不能協變和逆變的;

(Java的泛型是不支持協變和逆變的,只是能夠實現協變和逆變)(協變和逆變是指寬類型和窄類型在某種情況下(如參數、泛型、返回值)替換或交換的特性。簡單地說,協變是用一個窄類型替換寬類型,而逆變則是用寬類型覆蓋窄類型。子類覆寫父類返回值類型比父類型變窄,則是協變;子類覆寫父類型的參數類型變寬,則是逆變。數組支持協變,泛型不支持協變)。


建議98:建議採用的順序是List<T>,List<?>,List<Object>;

(1、List<T>是確定的某一個類型,編碼者知道它是一個類型,只是在運行期才確定而已;2、List<T>可以進行讀寫操作,List<?>是隻讀類型,因爲編譯器不知道List中容納的是什麼類型的元素,無法增加、修改,但是能刪除,List<Object>也可以讀寫操作,只是此時已經失去了泛型存在的意義了)。


建議99:嚴格限定泛型類型採用多重界限;

(使用“&”符號連接多個泛型界限,如:<T extends Staff & Passenger>)。


建議100:數組的真實類型必須是泛型類型的子類型;

(有可能會拋出ClassCastException異常,toArray方法返回後會進行一次類型轉換,Object數組轉換成了String數組。由於我們無法在運行期獲得泛型類型的參數,因此就需要調用者主動傳入T參數類型)。


建議101:注意Class類的特殊性;

Java處理的基本機制:先把Java源文件編譯成後綴爲class的字節碼文件,然後再通過ClassLoader機制把這些類文件加載到內存中,最後生成實例執行。Java使用一個元類(MetaClass)來描述加載到內存中的類數據,這就是Class類,它是一個描述類的類對象。Class類是“類中類”,具有特殊性:1、無構造函數,不能實例化,Class對象是在加載類時由Java虛擬機通過調用類加載器中的defineClass方法自動構建的;2、可以描述基本類型,8個基本類型在JVM中並不是一個對象,一般存在於棧內存中,但是Class類仍然可以描述它們,例如可以使用int.class表示int類型的類對象;3、其對象都是單例模式,一個Class的實例對象描述一個類,並且只描述一個類,反過來也成立,一個類只有一個Class實例對象。Class類是Java的反射入口,只有在獲得了一個類的描述對象後才能動態地加載、調用,一般獲得一個Class對象有三種途徑:1、類屬性方式,如String.class;2、對象的getClass方法,如new String().getClass();3、forName方法重載,如Class.forName("java.lang.String")。獲得了Class對象後,就可以通過getAnnotation()獲得註解,通過個體Methods()獲得方法,通過getConstructors()獲得構造函數等)。


建議102:適時選擇getDeclaredXXX和getXXX;

(getMethod方法獲得的是所有public訪問級別的方法,包括從父類繼承的方法,而getDeclaredMethod獲得的是自身類的所有方法,包括公用方法、私有方法等,而且不受限於訪問權限。Java之所以這樣處理,是因爲反射本意只是正常代碼邏輯的一種補充,而不是讓正常代碼邏輯產生翻天覆地的改動,所以public的屬性和方法最容易獲取,私有屬性和方法也可以獲取,但要限定本類。如果需要列出所有繼承自父類的方法,需要先獲得父類,然後調用getDeclaredMethods方法,之後持續遞歸)。


建議103:反射訪問屬性或方法是將Accessible設置爲true;

(通過反射方式執行方法時,必須在invoke之前檢查Accessible屬性。而Accessible屬性並不是我們語法層級理解的訪問權限,而是指是否更容易獲得,是否進行安全檢查。Accessible屬性只是用來判斷是否需要進行安全檢查的,如果不需要則直接執行,這就可以大幅度地提升系統性能。經過測試,在大量的反射情況下,設置Accessible爲true可以提升性能20倍以上)。


建議104:使用forName動態加載類文件;

(forName只是加載類,並不執行任何代碼)(動態加載(Dynamic Loading)是指在程序運行時加載需要的類庫文件,一般情況下,一個類文件在啓動時或首次初始化時會被加載到內存中,而反射則可以在運行時再決定是否要加載一個類,然後在JVM中加載並初始化。動態加載通常是通過Class.forName(String)實現。一個對象的生成必然會經過一下兩個步驟:1、加載到內存中生成Class的實例對象;2、通過new關鍵字生成實例對象;動態加載的意義:加載一個類即表示要初始化該類的static變量,特別是static代碼塊,在這裏我們可以做大量的工作,比如註冊自己,初始化環境等,這纔是我們重點關注的邏輯)。


建議105:動態加載不適合數組;

(通過反射操作數組使用Array類,不要採用通用的反射處理API)(如果forName要加載一個類,那它首先必須是一個類--8個基本類型排除在外,不是具體的類;其次,它必須具有可追索的類路徑,否則會報ClassNotFoundException異常。在Java中,數組是一個非常特殊的類,雖然是一個類,但沒有定義類路徑。作爲forName參數時會拋出ClassNotFoundException異常,原因是:數組雖然是一個類,在聲明時可以定義爲String[],但編譯器編譯後會爲不同的數組類型生成不同的類,所以要想動態創建和訪問數組,基本的反射是無法實現的)。


建議106:動態代理可以使代理模式更加靈活;

(Java的反射框架提供了動態代理(Dynamic Proxy)機制,允許在運行期對目標類生成代理,避免重複開發。靜態代理是通過代理主題角色(Proxy)和具體主題角色(Real Subject)共同實現抽象主題角色(Subject)的邏輯的,只是代理主題角色把相關的執行邏輯委託給了具體主題角色而已。動態代理需要實現InvocationHandler接口,必須要實現invoke方法,該方法完成了對真實方法的調用)。


建議107:使用反射增加裝飾模式的普適性;

裝飾模式(Decorator Pattern)的定義是“動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比於生成子類更爲靈活”。比較通用的裝飾模式,只需要定義被裝飾的類及裝飾類即可,裝飾行爲由動態代理實現,實現了對裝飾類和被裝飾類的完全解耦,提供了系統的擴展性)。


建議108:反射讓模板方法模式更強大;

(決定使用模板方法模式時,請嘗試使用反射方式實現,它會讓你的程序更靈活、更強大)(模板方法模式(Template Method Pattern)的定義是:定義一個操作中的算法骨架,將一些步驟延遲到子類中,使子類不改變一個算法的結構即可重定義該算法的某些特定步驟。簡單說,就是父類定義抽象模板作爲骨架,其中包括基本方法(是由子類實現的方法,並且在模板方法被調用)和模板方法(實現對基本方法的調度,完成固定的邏輯),它使用了簡單的繼承和覆寫機制。使用反射後,不需要定義任何抽象方法,只需定義一個基本方法鑑別器即可加載複合規則的基本方法)。


建議109:不需要太多關注反射效率;

(反射效率低是個真命題,但因爲這一點而不使用它就是個假命題)(反射效率相對於正常的代碼執行確實低很多(經測試,相差15倍左右),但是它是一個非常有效的運行期工具類)。


第8章  異常

建議110:提倡異常封裝;(異常封裝有三方面的優點:1、提高系統的友好性;2、提高系統的可維護性;3、解決Java異常機制本身的缺陷);


建議111:採用異常鏈傳遞異常;

責任鏈模式(Chain of Responsibility),目的是將多個對象連城一條鏈,並沿着這條鏈傳遞該請求,直到有對象處理它爲止,異常的傳遞處理也應該採用責任鏈模式)。


建議112:受檢異常儘可能轉化爲非受檢異常;

(受檢異常威脅到系統的安全性、穩定性、可靠性、正確性時、不能轉爲非受檢異常)(受檢異常(Checked Exception),非受檢異常(Unchecked Exception),受檢異常時正常邏輯的一種補償處理手段,特別是對可靠性要求比較高的系統來說,在某些條件下必須拋出受檢異常以便由程序進行補償處理,也就是說受檢異常有合理的存在理由。但是受檢異常有不足的地方:1、受檢異常使接口聲明脆弱;2、受檢異常是代碼的可讀性降低,一個方法增加了受檢異常,則必須有一個調用者對異常進行處理。受檢異常需要try..catch處理;3、受檢異常增加了開發工作量。避免以上受檢異常缺點辦法:將受檢異常轉化爲非受檢異常)。


建議113:不要在finally塊中處理返回值;

(在finally塊中加入了return語句會導致以下兩個問題:1、覆蓋了try代碼塊中的return返回值;2、屏蔽異常,即使throw出去了異常,異常線程會登記異常,但是當執行器執行finally代碼塊時,則會重新爲方法賦值,也就是告訴調用者“該方法執行正確”,沒有發生異常,於是乎,異常神奇的消失了)。


建議114:不要在構造函數中拋異常;

(Java異常機制有三種:1、Error類及其子類表示的是錯誤,它是不需要程序員處理的也不能處理的異常,比如VirtualMachineError虛擬機錯誤,ThreadDeath線程僵死等;2、RuntimeException類及其子類表示的是非受檢異常,是系統可能拋出的異常,程序員可以去處理,也可以不處理,最經典的是NullPointerException空指針異常和IndexOutOfBoundsException越界異常;3、Exception類及其子類(不包含非受檢異常)表示的是受檢異常,這是程序員必須要處理的異常,不處理則程序不能通過編譯,比如IOException表示I/O異常,SQLException數據庫訪問異常。一個對象的創建過程要經過內存分配、靜態代碼初始化、構造函數執行等過程,構造函數中是否允許拋出異常呢?從Java語法上來說,完全可以,三類異常都可以,但是從系統設計和開發的角度分析,則儘量不要在構造函數中拋出異常)。


建議115:使用Throwable獲得棧信息;

(在出現異常時(或主動聲明一個Throwable對象時),JVM會通過fillInStackTrace方法記錄下棧信息,然後生成一個Throwable對象,這樣就能知道類間的調用順序、方法名稱以及當前行號等)。


建議116:異常只爲異常服務;

(異常原本是正常邏輯的一個補充,但有時候會被當前主邏輯使用。異常作爲主邏輯有問題:1、異常判斷降低了系統性能;2、降低了代碼的可讀性,只有詳細瞭解valueOf方法的人才能讀懂這樣的代碼,因爲valueOf拋出的是一個非受檢異常;3、隱藏了運行期可能產生的錯誤,catch到異常,但沒有做任何處理)。


建議117:多使用異常,把性能問題放一邊;

(new一個IOException會被String慢5倍:因爲它要執行fillInStackTrace方法,要記錄當前棧的快照,而String類則是直接申請一個內存創建對象。而且,異常類是不能緩存的。但是異常是主邏輯的例外邏輯,會讓方法更符合實際的處理邏輯,同時使主邏輯更加清晰,可讓正常代碼和異常代碼分離、能快速查找問題(棧信息快照)等)。


第9章  多線程和併發

建議118:不推薦覆寫start方法;

(繼承自Thread類的多線程類不必覆寫start方法。原本的start方法中,調用了本地方法start0,它實現了啓動線程、申請棧內存、運行run方法、修改線程狀態等職責,線程管理和棧內存管理都是由JVM實現的,如果覆蓋了start方法,也就是撤銷了線程管理和棧內存管理的能力。所以除非必要,不然不要覆寫start方法,即使需要覆寫start方法,也需要在方法體內加上super.start調用父類中的start方法來啓動默認的線程操作)。


建議119:啓動線程前stop方法是不可靠的;

(現象:使用stop方法停止一個線程,而stop方法在此處的目的不是停止一個線程,而是設置線程爲不可啓用狀態。但是運行結果出現奇怪現象:部分線程還是啓動了,也就是在某些線程(沒有規律)中的start方法正常執行了。在不符合判斷規則的情況下,不可啓用狀態的線程還是啓用了,這是線程啓動(start方法)一個缺陷。Thread類的stop方法會根據線程狀態來判斷是終結線程還是設置線程爲不可運行狀態,對於未啓動的線程(線程狀態爲NEW)來說,會設置其標誌位爲不可啓動,而其他的狀態則是直接停止。start方法源碼中,start0方法在stop0方法之前,也就是說即使stopBeforeStart爲true(不可啓動),也會先啓動一個線程,然後再stop0結束這個線程,而罪魁禍首就在這裏!所以不要使用stop方法進行狀態的設置)。


建議120:不適用stop方法停止線程;

(線程啓動完畢後,需要停止,Java只提供了一個stop方法,但是不建議使用,有以下三個問題:1、stop方法是過時的;2、stop方法會導致代碼邏輯不完整,stop方法是一種“惡意”的中斷,一旦執行stop方法,即終止當前正在運行的線程,不管線程邏輯是否完整,這是非常危險的,以爲stop方法會清除棧內信息,結束該線程,但是可能該線程的一段邏輯非常重,比如子線程的主邏輯、資源回收、情景初始化等,因爲stop線程了,這些都不會再執行。子線程執行到何處會被關閉很難定位,這爲以後的維護帶來了很多麻煩;3、stop方法會破壞原子邏輯,多線程爲了解決共享資源搶佔的問題,使用了鎖概念,避免資源不同步,但是stop方法會丟棄所有的鎖,導致原子邏輯受損。Thread提供的interrupt中斷線程方法,它不能終止一個正在執行着的線程,它只是修改中斷標誌唯一。總之,期望終止一個正在運行的線程,不能使用stop方法,需要自行編碼實現。如果使用線程池(比如ThreadPoolExecutor類),那麼可以通過shutdown方法逐步關閉池中的線程)。


建議121:線程優先級只使用三個等級;

線程優先級推薦使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三個級別,不建議使用其他7個數字)(線程的優先級(Priority)決定了線程獲得CPU運行的機會,優先級越高,運行機會越大。事實:1、並不是嚴格尊重線程優先級別來執行的,分爲10個級別;2、優先級差別越大,運行機會差別越大;對於Java來說,JVM調用操作系統的接口設置優先級,比如Windows是通過調用SetThreadPriority函數來設置的。不同操作系統線程優先級設置是不相同的,Windows有7個優先級,Linux有140個優先級,Freebsd有255個優先級。Java締造者也發現了該問題,於是在Thread類中設置了三個優先級,建議使用優先級常量,而不是1到10隨機的數字)。


建議122:使用線程異常處理器提升系統可靠性;

(可以使用線程異常處理器來處理相關異常情況的發生,比如當機自動重啓,大大提高系統的可靠性。在實際環境中應用注意以下三點:1、共享資源鎖定;2、髒數據引起系統邏輯混亂;3、內存溢出,線程異常了,但由該線程創建的對象並不會馬上回收,如果再重新啓動新線程,再創建一批新對象,特別是加入了場景接管,就危險了,有可能發生OutOfMemory內存泄露問題)。


建議123:volatile不能保證數據同步;

(volatile不能保證數據是同步的,只能保證線程能夠獲得最新值)(volatile關鍵字比較少用的原因:1、Java1.5之前該關鍵字在不同的操作系統上有不同的表現,移植性差;2、比較難設計,而且誤用較多。在變量錢加上一個volatile關鍵字,可以確保每個線程對本地變量的訪問和修改都是直接與主內存交互的,而不是與本地線程的工作內存交互的,保證每個線程都能獲得最“新鮮”的變量值。但是volatile關鍵字並不能保證線程安全,它只能保證當前線程需要該變量的值時能夠獲得最新的值,而不能保證多個線程修改的安全性)。


建議124:異步運算考慮使用Callable接口;

(多線程應用的兩種實現方式:一種是實現Runnable接口,另一種是繼承Thread類,這兩個方式都有缺點:run方法沒有返回值,不能拋出異常(歸根到底是Runnable接口的缺陷,Thread也是實現了Runnable接口),如果需要知道一個線程的運行結果就需要用戶自行設計,線程類本身也不能提供返回值和異常。Java1.5開始引入了新的接口Callable,類似於Runnable接口,實現它就可以實現多線程任務,實現Callable接口的類,只是表明它是一個可調用的任務,並不表示它具有多線程運算能力,還是需要執行器來執行的)。


建議125:優先選擇線程池;

(Java1.5以前,實現多線程比較麻煩,需要自己啓動線程,並關注同步資源,防止出現線程死鎖等問題,Java1.5以後引入了並行計算框架,大大簡化了多線程開發。線程有五個狀態:新建狀態(New)、可運行狀態(Runnable,也叫作運行狀態)、阻塞狀態(Blocked)、等待狀態(Waiting)、結束狀態(Terminated),線程的狀態只能由新建轉變爲運行態後纔可能被阻塞或等待,最後終結,不可能產生本末倒置的情況,比如想把結束狀態變爲新建狀態,則會出現異常。線程運行時間分爲三個部分:T1爲線程啓動時間;T2爲線程體運行時間;T3爲線程銷燬時間。每次創建線程都會經過這三個時間會大大增加系統的響應時間。T2是無法避免的,只能通過優化代碼來降低運行時間。T1和T3都可以通過線程池(Thread Pool)來縮短時間。線程池的實現涉及一下三個名詞:1、工作線程(Worker),線程池中的線程只有兩個狀態:可運行狀態和等待狀態;2、任務接口(Task),每個任務必須實現的接口,以供工作線程調度器調度,它主要規定了任務的入口、任務執行完的場景處理、任務的執行狀態等。這裏的兩種類型的任務:具有返回值(或異常)的Callable接口任務和無返回值併兼容舊版本的Runnable接口任務;3、任務隊列(Work Queue),也叫作工作隊列,用於存放等待處理的任務,一般是BlockingQueue的實現類,用來實現任務的排隊處理。線程池的創建過程:創建一個阻塞隊列以容納任務,在第一次執行任務時闖將足夠多的線程(不超過許可線程數),並處理任務,之後每個工作線程自行從任務隊列中獲得任務,直到任務隊列中任務數量爲0爲止,此時,線程將處於等待狀態,一旦有任務加入到隊列中,即喚醒工作線程進行處理,實現線程的可複用性)。


建議126:適時選擇不同的線程池來實現;

(Java的線程池實現從根本上來說只有兩個:ThreadPoolExecutor類和ScheduledThreadPoolExecutor類,還是父子關係。爲了簡化並行計算,Java還提供了一個Executors的靜態類,它可以直接生成多種不同的線程池執行器,比如單線程執行器、帶緩衝功能的執行器等,歸根結底還是以上兩個類的封裝類)。


建議127:Lock與synchronized是不一樣的;

Lock類(顯式鎖)synchronized關鍵字(內部鎖)用在代碼塊的併發性和內存上時的語義是一樣的,都是保持代碼塊同時只有一個線程具有執行權。顯式鎖的鎖定和釋放必須在一個try...finally塊中,這是爲了確保即使出現運行期異常也能正常釋放鎖,保證其他線程能夠順利執行。Lock鎖爲什麼不出現互斥情況,所有線程都是同時執行的?原因:這是因爲對於同步資源來說,顯式鎖是對象級別的鎖,而內部鎖是類級別的鎖,也就是說Lock鎖是跟隨對象的synchronized鎖是跟隨類的,更簡單地說把Lock定義爲多線程類的私有屬性是起不到資源互斥作用的,除非是把Lock定義爲所有線程共享變量。除了以上不同點之外,還有以下4點不同:1、Lock支持更細粒度的鎖控制,假設讀寫鎖分離,寫操作時不允許有讀寫操作存在,而讀操作時讀寫可以併發執行,這一點內部鎖很難實現;2、Lock是無阻塞鎖,synchronized是阻塞鎖,線程A持有鎖,線程B也期望獲得鎖時,如果爲Lock,則B線程爲等待狀態,如果爲synchronized,則爲阻塞狀態;3、Lock可實現公平鎖,synchronized只能是非公平鎖什麼叫做非公平鎖?當一個線程A持有鎖,而線程B、C處於阻塞(或等待)狀態時,若線程A釋放鎖。JVM將從線程B、C中隨機選擇一個線程持有鎖並使其獲得執行權,這叫做非公平鎖(因爲它拋棄了先來後到的順序);若JVM選擇了等待時間最長的一個線程持有鎖,則爲公平鎖。需要注意的是,即使是公平鎖,JVM也無法準確做到“公平”,在程序中不能以此作爲精確計算。顯式鎖默認是非公平鎖,但可以在構造函數中加入參數true來聲明出公平鎖;4、Lock是代碼級的,synchronized是JVM級的,Lock是通過編碼實現的,synchronized是在運行期由JVM解釋的,相對來說synchronized的優化可能性更高,畢竟是在最核心不爲支持的,Lock的優化需要用戶自行考慮。相對來說,顯式鎖使用起來更加便利和強大,在實際開發中選擇哪種類型的鎖就需要根據實際情況考慮了:靈活、強大則選擇Lock,快捷、安全則選擇synchronized)。


建議128:預防線程死鎖;

線程死鎖(DeadLock)是多線程編碼中最頭疼問題,也是最難重現的問題,因爲Java是單進程多線程語言。要達到線程死鎖需要四個條件:1、互斥條件;2、資源獨佔條件;3、不剝奪條件;4、循環等待條件;按照以下兩種方式來解決:1、避免或減少資源貢獻;2、使用自旋鎖,如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將“自旋”在那裏,直到該自旋鎖的保持者釋放了鎖爲止)。


建議129:適當設置阻塞隊列長度;

阻塞隊列BlockingQueue擴展了Queue、Collection接口,對元素的插入和提取使用了“阻塞”處理。但是BlockingQueue不能夠自行擴容,如果隊列已滿則會報IllegalStateException:Queue full隊列已滿異常;這是阻塞隊列非阻塞隊列一個重要區別:阻塞隊列的容量是固定的,非阻塞隊列則是變長的。阻塞隊列可以在聲明時指定隊列的容量,若指定的容量,則元素的數量不可超過該容量,若不指定,隊列的容量爲Integer的最大值。有此區別的原因是:阻塞隊列是爲了容納(或排序)多線程任務而存在的,其服務的對象是多線程應用,而非阻塞隊列容納的則是普通的數據元素。阻塞隊列的這種機制對異步計算是非常有幫助的,如果阻塞隊列已滿,再加入任務則會拒絕加入,而且返回異常,由系統自行處理,避免了異步計算的不可知性。可以使用put方法,它會等隊列空出元素,再讓自己加入進去,無論等待多長時間都要把該元素插入到隊列中,但是此種等待是一個循環,會不停地消耗系統資源,當等待加入的元素數量較多時勢必會對系統性能產生影響。offer方法可以優化一下put方法)。


建議130:使用CountDownLatch協調子線程;

CountDownLatch協調子線程步驟:一個開始計數器,多個結束計數器:1、每一個子線程開始運行,執行代碼到begin.await後線程阻塞,等待begin的計數變爲0;2、主線程調用begin的countDown方法,使begin的計數器爲0;3、每個線程繼續運行;4、主線程繼續運行下一條語句,end的計數器不爲0,主線程等待;5、每個線程運行結束時把end的計數器減1,標誌着本線程運行完畢;6、多個線程全部結束,end計數器爲0;7、主線程繼續執行,打印出結果。類似:領導安排了一個大任務給我,我一個人不可能完成,於是我把該任務分解給10個人做,在10個人全部完成後,我把這10個結果組合起來返回給領導--這就是CountDownLatch的作用)。


建議131:CyclicBarrier讓多線程齊步走;

(CyclicBarrier關卡可以讓所有線程全部處於等待狀態(阻塞),然後在滿足條件的情況下繼續執行,這就好比是一條起跑線,不管是如何到達起跑線的,只要到達這條起跑線就必須等待其他人員,待人員到齊後再各奔東西,CyclicBarrier關注的是匯合點的信息,而不在乎之前或者之後做何處理。CyclicBarrier可以用在系統的性能測試中,測試併發性)。


第10章  性能和效率

建議132:提升Java性能的基本方法;

(如何讓Java程序跑的更快、效率更高、吞吐量更大:1、不要在循環條件中計算,每循環一次就會計算一次,會降低系統效率;2、儘可能把變量、方法聲明爲final static類型,加上final static修飾後,在類加載後就會生成,每次方法調用則不再重新生成對象了;3、縮小變量的作用範圍,目的是加快GC的回收;4、頻繁字符串操作使用StringBuilder或StringBuffer5、使用非線性檢索,使用binarySearch查找會比indexOf查找元素快很多,但是使用binarySearch查找時記得先排序;6、覆寫Exception的fillInStackTrace方法fillInStackTrace方法是用來記錄異常時的棧信息的,這是非常耗時的動作,如果不需要關注棧信息,則可以覆蓋,以提升性能;7、不建立冗餘對象)。


建議133:若非必要,不要克隆對象;

(克隆對象並不比直接生成對象效率高)(通過clone方法生成一個對象時,就會不再執行構造函數了,只是在內存中進行數據塊的拷貝,看上去似乎應該比new方法的性能好很多,但事實上,一般情況下new生成的對象比clone生成的性能方面要好很多。JVM對new做了大量的系能優化,而clone方式只是一個冷僻的生成對象的方式,並不是主流,它主要用於構造函數比較複雜,對象屬性比較多,通過new關鍵字創建一個對象比較耗時間的時候)。


建議134:推薦使用“望聞問切”的方式診斷性能;

性能診斷遵循“望聞問切”,不可過度急躁)。


建議135:必須定義性能衡量標準;

(原因:1、性能衡量標準是技術與業務之間的契約;2、性能衡量標誌是技術優化的目標)。


建議136:槍打出頭鳥--解決首要系統性能問題;

(解決性能優化要“單線程”小步前進,避免關注點過多而導致精力分散)(解決性能問題時,不要把所有的問題都擺在眼前,這隻會“擾亂”你的思維,集中精力,找到那個“出頭鳥”,解決它,在大部分情況下,一批性能問題都會迎刃而解)。


建議137:調整JVM參數以提升性能;

四個常用的JVM優化手段:

1、調整堆內存大小,JVM兩種內存:棧內存(Stack)堆內存(Heap)棧內存的特點是空間小,速度快,用來存放對象的引用及程序中的基本類型;而堆內存的特點是空間比較大,速度慢一般對象都會在這裏生成、使用和消亡。棧空間由線程開闢,線程結束,棧空間由JVM回收,它的大小一般不會對性能有太大影響,但是它會影響系統的穩定性,超過棧內存的容量時,會拋StackOverflowError錯誤。可以通過“java -Xss <size>”設置棧內存大小來解決。堆內存的調整不能太隨意,調整得太小,會導致Full GC頻繁執行,輕則導致系統性能急速下降,重則導致系統根本無法使用;調整得太大,一則浪費資源(若設置了最小堆內存則可以避免此問題),二則是產生系統不穩定的情況,設置方法“java -Xmx1536 -Xms1024m”,可以通過將-Xmx和-Xms參數值設置爲相同的來固定堆內存大小;

2、調整堆內存中各分區的比例,JVM的堆內存包括三部分:新生區(Young Generation Space)、養老區(Tenure Generation Space)、永久存儲區(Permanent Space 方法區),其中新生成的對象都在新生區,又分爲伊甸區(Eden Space)、倖存0區(Survivor 0 Space)和倖存1區(Survivor 1 Space),當在程序中使用了new關鍵字時,首先在Eden區生成該對象,如果Eden區滿了,則觸發minor GC,然後把剩餘的對象移到Survivor區(0區或者1區),如果Survivor取也滿了,則minor GC會再回收一次,然後再把剩餘的對象移到養老區,如果養老區也滿了,則會觸發Full GC(非常危險的動作,JVM會停止所有的執行,所有系統資源都會讓位給垃圾回收器),會對所有的對象過濾一遍,檢查是否有可以回收的對象,如果還是沒有的話,就拋出OutOfMemoryError錯誤。一般情況下新生區與養老區的比例爲1:3左右,設置命令:“java -XX:NewSize=32m -XX:MaxNewSize=640m -XX:MaxPermSize=1280m -XX:NewRatio=5”,該配置指定新生代初始化爲32MB(也就是新生區最小內存爲32M),最大不超過640MB,養老區最大不超過1280MB,新生區和養老區的比例爲1:5.一般情況下Eden Space : Survivor 0 Space : Survivor 1 Space == 8 : 1 : 1);

3、變更GC的垃圾回收策略,設置命令“java -XX:+UseParallelGC -XX:ParallelGCThreads=20”,這裏啓用了並行垃圾收集機制,並且定義了20個收集線程(默認的收集線程等於CPU的數量),這對多CPU的系統時非常有幫助的,可以大大減少垃圾回收對系統的影響,提高系統性能;

4、更換JVM,如果所有的JVM優化都不見效,那就只有更換JVM了,比較流行的三個JVM產品:Java HotSpot VM、Oracle JRockit JVM、IBM JVM。


建議138:性能是個大“咕咚”;

(1、沒有慢的系統,只有不滿足義務的系統;2、沒有慢的系統,只有架構不良的系統;3、沒有慢的系統,只有懶惰的技術人員;4、沒有慢的系統,只有不願意投入的系統)。


第11章  開源世界

建議139:大膽採用開源工具;

(選擇開源工具和框架時要遵循一定的規則:1、普適性原則;2、唯一性原則;3、“大樹納涼”原則;4、精而專原則;5、高熱度原則)。


建議140:推薦使用Guava擴展工具包

(Guava(石榴)是Google發佈的,其中包含了collections、caching、primitives support、concurrency libraries、common annotations、I/O等)。


建議141:Apache擴展包;

(Apache Commons通用擴展包基本上是每個項目都會使用的,一般情況下lang包用作JDK的基礎語言擴展。Apache Commons項目包含非常好用的工具,如DBCP、net、Math等)。


建議142:推薦使用Joda日期時間擴展包;

(Joda可以很好地與現有的日期類保持兼容,在需要複雜的日期計算時使用Joda。日期工具類也可以選擇date4j)。


建議143:可以選擇多種Collections擴展;

(三個比較有個性的Collections擴展工具包:1、FastUtil,主要提供兩種功能:一種是限定鍵值類型的Map、List、Set等,另一種是大容量的集合;2、Trove,提供了一個快速、高效、低內存消耗的Collection集合,並且還提供了過濾和攔截功能,同時還提供了基本類型的集合;3、lambdaj,是一個純淨的集合操作工具,它不會提供任何的集合擴展,只會提供對集合的操作,比如查詢、過濾、統一初始化等)。


第12章  思想爲源

建議144:提倡良好的代碼風格;

(良好的編碼風格包括:1、整潔;2、統一;3、流行;4、便捷,推薦使用Checkstyle檢測代碼是否遵循規範)。


建議145:不要完全依靠單元測試來發現問題;

(單元測試的目的是保證各個獨立分隔的程序單元的正確性,雖然它能夠發現程序中存在的問題(或缺陷、或錯誤),但是單元測試只是排查程序錯誤的一種方式,不能保證代碼中的所有錯誤都能被單元測試挖掘出來,原因:1、單元測試不可能測試所有的場景(路徑);2、代碼整合錯誤是不可避免的;3、部分代碼無法(或很難)測試;4、單元測試驗證的是編碼人員的假設)。


建議146:讓註釋正確、清晰、簡潔;

(註釋不是美化劑,而是催化劑,或爲優秀加分,或爲拙略減分)。


建議147:讓接口的職責保持單一;

(接口職責一定要單一,實現類職責儘量單一)(單一職責原則(Single Responsibility Principle,簡稱SRP)有以下三個優點:1、類的複雜性降低;2、可讀性和可維護性提高;3、降低變更風險)。


建議148:增強類的可替換性

(Java的三大特徵:封裝、繼承、多態;說說多態,一個接口可以有多種實現方式,一個父類可以有多個子類,並且可以把不同的實現或子類賦給不同的接口或父類。多態的好處非常多,其中一點就是增強了類的可替換性,但是單單一個多態特性,很難保證我們的類是完全可以替換的,幸好還有一個里氏替換原則來約束。里氏替換原則:所有引用基類的地方必須能透明地使用其子類的對象。通俗點講,只要父類型能出現的地方子類型就可以出現,而且將父類型替換爲子類型還不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類型還是子類型。爲了增強類的可替換性,在設計類時需要考慮以下三點:1、子類型必須完全實現父類型的方法;2、前置條件可以被放大;3、後置條件可以被縮小)。


建議149:依賴抽象而不是實現;

(此處的抽象是指物體的抽象,比如出行,依賴的是抽象的運輸能力,而不是具體的運輸交通工具。依賴倒置原則(Dependence Inversion Principle,簡稱DIP)要求實現解耦,保持代碼間的鬆耦合,提高代碼的複用率。DIP的原始定義包含三層含義:1、高層模塊不應該依賴底層模塊,兩者都應該依賴其抽象;2、抽象不應該依賴細節;3、細節應該依賴抽象;DIP在Java語言中的表現就是:1、模塊間的依賴是通過抽象發生的,實現類之間不發生直接的依賴關係,其依賴關係是通過接口或抽象類產生的;2、接口或抽象類不依賴於實現類;3、實現類依賴接口或抽象類;更加精簡的定義就是:面向接口編程。實現模塊間的鬆耦合遵循規則:1、儘量抽象;2、表面類型必須是抽象的;3、任何類都不應該從具體類派生;4、儘量不要覆寫基類的方法;5、抽象不關注細節)。


建議150:拋棄7條不良的編碼習慣;

1、自由格式的代碼;2、不使用抽象的代碼;3、彰顯個性的代碼;4、死代碼;5、冗餘代碼;6、拒絕變化的代碼;7、自以爲是的代碼)。


建議151:以技術人員自律而不是工人;

(20條建議:1、熟悉工具;2、使用IDE;3、堅持編碼;4、編碼前思考;5、堅持重構;6、多寫文檔;7、保持程序版本的簡單性;8、做好備份;9、做單元測試;10、不要重複發明輪子;11、不要拷貝;12、讓代碼充滿靈性;13、測試自動化;14、做壓力測試;15、“剽竊”不可恥;16、堅持向敏捷學習;17、重裏更重面;18、分享;19、刨根問底;20、橫向擴展)。

發佈了13 篇原創文章 · 獲贊 12 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章