寫著寫著發(fā)現(xiàn)簡書提醒我文章接近字?jǐn)?shù)極限,建議我換一篇寫了。
建議52:推薦使用String直接量賦值
一般對象都是通過new關(guān)鍵字生成的,但是String還有第二種生成方式,也就是我們經(jīng)常使用的直接聲明方式,這種方式是極力推薦的,但不建議使用new String("A")的方式賦值。為什么呢?我們看如下代碼:
public class Client58 {
public static void main(String[] args) {
String str1 = "詹姆斯";
String str2 = "詹姆斯";
String str3 = new String("詹姆斯");
String str4 = str3.intern();
// 兩個(gè)直接量是否相等
System.out.println(str1 == str2);
// 直接量和對象是否相等
System.out.println(str1 == str3);
// 經(jīng)過intern處理后的對象與直接量是否相等
System.out.println(str1 == str4);
}
}
注意看上面的程序,我們使用"=="判斷的是兩個(gè)對象的引用地址是否相同,也就是判斷是否為同一個(gè)對象,打印的結(jié)果是true,false,true。即有兩個(gè)直接量是同一個(gè)對象(進(jìn)過intern處理后的String與直接量是同一個(gè)對象),但直接通過new生成的對象卻與之不等,原因何在?
原因是Java為了避免在一個(gè)系統(tǒng)中大量產(chǎn)生String對象(為什么會大量產(chǎn)生,因?yàn)镾tring字符串是程序中最經(jīng)常使用的類型),于是就設(shè)計(jì)了一個(gè)字符串池(也叫作字符串常量池,String pool或String Constant Pool或String Literal Pool),在字符串池中容納的都是String字符串對象,它的創(chuàng)建機(jī)制是這樣的:創(chuàng)建一個(gè)字符串時(shí),首先檢查池中是否有字面值相等的字符串,如果有,則不再創(chuàng)建,直接返回池中該對象的引用,若沒有則創(chuàng)建之,然后放到池中,并返回新建對象的引用,這個(gè)池和我們平常說的池非常接近。對于此例子來說,就是創(chuàng)建第一個(gè)"詹姆斯"字符串時(shí),先檢查字符串池中有沒有該對象,發(fā)現(xiàn)沒有,于是就創(chuàng)建了"詹姆斯"這個(gè)字符串并放到池中,待創(chuàng)建str2字符串時(shí),由于池中已經(jīng)有了該字符串,于是就直接返回了該對象的引用,此時(shí),str1和str2指向的是同一個(gè)地址,所以使用"=="來判斷那當(dāng)然是相等的了。
那為什么使用new String("詹姆斯")就不相等了呢?因?yàn)橹苯勇暶饕粋€(gè)String對象是不檢查字符串池的,也不會把對象放到字符串池中,那當(dāng)然"=="為false了。
那為什么intern方法處理后即又相等了呢?因?yàn)閕ntern會檢查當(dāng)前對象在對象池中是否存在字面值相同的引用對象,如果有則返回池中的對象,如果沒有則放置到對象池中,并返回當(dāng)前對象。
可能有人要問了,放到池中,是不是要考慮垃圾回收問題呀?不用考慮了,雖然Java的每個(gè)對象都保存在堆內(nèi)存中但是字符串非常特殊,它在編譯期已經(jīng)決定了其存在JVM的常量池(Constant Pool),垃圾回收不會對它進(jìn)行回收的。
通過上面的介紹,我們發(fā)現(xiàn)Java在字符串的創(chuàng)建方面確實(shí)提供了非常好的機(jī)制,利用對象池不僅可以提高效率,同時(shí)減少了內(nèi)存空間的占用,建議大家在開發(fā)中使用直接量賦值方式,除非必要才建立一個(gè)String對象。
建議54:正確使用String、StringBuffer、StringBuilder
CharSequence接口有三個(gè)實(shí)現(xiàn)類與字符串有關(guān),String、StringBuffer、StringBuilder,雖然它們都與字符串有關(guān),但其處理機(jī)制是不同的。
String類是不可變的量,也就是創(chuàng)建后就不能再修改了,比如創(chuàng)建了一個(gè)"abc"這樣的字符串對象,那么它在內(nèi)存中永遠(yuǎn)都會是"abc"這樣具有固定表面值的一個(gè)對象,不能被修改,即使想通過String提供的方法來嘗試修改,也是要么創(chuàng)建一個(gè)新的字符串對象,要么返回自己,比如:
String str = "abc";
String str1 = str.substring(1);
其中str是一個(gè)字符串對象,其值是"abc",通過substring方法又重新生成了一個(gè)字符串str1,它的值是"bc",也就是說str引用的對象一但產(chǎn)生就永遠(yuǎn)不會變。為什么上面還說有可能不創(chuàng)建對象而返回自己呢?那是因?yàn)椴捎胹ubstring(0)就不會創(chuàng)建對象。JVM從字符串池中返回str的引用,也就是自身的引用。
StringBuffer是一個(gè)可變字符串,它與String一樣,在內(nèi)存中保存的都是一個(gè)有序的字符序列(char 類型的數(shù)組),不同點(diǎn)是StringBuffer對象的值是可改變的,例如:
StringBuffer sb = new StringBuffer("a");
sb.append("b");
從上面的代碼可以看出sb的值在改變,初始化的時(shí)候是"a" ,經(jīng)過append方法后,其值變成了"ab"。可能有人會問了,這與String類通過 "+" 連接有什么區(qū)別呢?例如:
String s = "a";
s = s + "b";
有區(qū)別,字符串變量s初始化時(shí)是 "a" 對象的引用,經(jīng)過加號計(jì)算后,s變量就修改為了 “ab” 的引用,但是初始化的 “a” 對象還沒有改變,只是變量s指向了新的引用地址,再看看StringBuffer的對象,它的引用地址雖不變,但值在改變。
StringBuffer和StringBuilder基本相同,都是可變字符序列,不同點(diǎn)是:StringBuffer是線程安全的,StringBuilder是線程不安全的,翻翻兩者的源代碼,就會發(fā)現(xiàn)在StringBuffer的方法前都有關(guān)鍵字syschronized,這也是StringBuffer在性能上遠(yuǎn)遠(yuǎn)低于StringBuffer的原因。
在性能方面,由于String類的操作都是產(chǎn)生String的對象,而StringBuilder和StringBuffer只是一個(gè)字符數(shù)組的再擴(kuò)容而已,所以String類的操作要遠(yuǎn)慢于StringBuffer 和 StringBuilder。
弄清楚了三者之間的原理,我們就可以在不同的場景下使用不同的字符序列了:
- 使用String類的場景:在字符串不經(jīng)常變化的場景中可以使用String類,例如常量的聲明、少量的變量運(yùn)算等;
- 使用StringBuffer的場景:在頻繁進(jìn)行字符串的運(yùn)算(如拼接、替換、刪除等),并且運(yùn)行在多線程的環(huán)境中,則可以考慮使用StringBuffer,例如XML解析、HTTP參數(shù)解析和封裝等;
-
使用StringBuilder的場景:在頻繁進(jìn)行字符串的運(yùn)算(如拼接、替換、刪除等),并且運(yùn)行在單線程的環(huán)境中,則可以考慮使用StringBuilder,如SQL語句的拼接,JSON封裝等。
**注意:在適當(dāng)?shù)膱鼍斑x用字符串類型 **
事實(shí)上這個(gè)問題被多個(gè)地方研究了很多次,我自己也寫了一篇專門的文章來介紹String類:
http://www.lxweimin.com/p/e494552f2cf0
建議55:注意字符串的位置
看下面一段程序:
public class Client55 {
public static void main(String[] args) {
String str1 = 1 + 2 + "apples";
String str2 = "apples" + 1 + 2;
System.out.println(str1);
System.out.println(str2);
}
}
想想兩個(gè)字符串輸出的結(jié)果的蘋果數(shù)量是否一致,如果一致,會是幾呢?
答案是不一致,str1的值是"3apples" ,str2的值是“apples12”,這中間懸殊很大,只是把“apples” 調(diào)換了一下位置,為何會發(fā)生如此大的變化呢?
這都源于java對于加號的處理機(jī)制:在使用加號進(jìn)行計(jì)算的表達(dá)式中,只要遇到String字符串,則所有的數(shù)據(jù)都會轉(zhuǎn)換為String類型進(jìn)行拼接,如果是原始數(shù)據(jù),則直接拼接,如是是對象,則調(diào)用toString方法的返回值然后拼接,如:
str = str + new ArrayList();
上面就是調(diào)用ArrayList對象的toString方法返回值進(jìn)行拼接的。再回到前面的問題上,對與str1 字符串,Java的執(zhí)行順序是從左到右,先執(zhí)行1+2,也就是算術(shù)加法運(yùn)算,結(jié)果等于3,然后再與字符串進(jìn)行拼接,結(jié)果就是 "3 apples",其它形式類似于如下計(jì)算:
String str1 = (1 + 2 ) + "apples" ;
而對于str2字符串,由于第一個(gè)參與運(yùn)算的是String類型,加1后的結(jié)果是“apples 1” ,這仍然是一個(gè)字符串,然后再與2相加,結(jié)果還是一個(gè)字符串,也就是“apples12”。這說明如果第一個(gè)參數(shù)是String,則后續(xù)的所有計(jì)算都會轉(zhuǎn)變?yōu)镾tring類型,誰讓字符串是老大呢!
注意: 在“+” 表達(dá)式中,String字符串具有最高優(yōu)先級。
建議57:推薦在復(fù)雜字符串操作中使用正則表達(dá)式
這是一個(gè)很自然的選擇,因?yàn)檎齽t表達(dá)式實(shí)在是太強(qiáng)大了。
建議58:強(qiáng)烈建議使用UTF編碼
Java的亂碼問題由來已久,有經(jīng)驗(yàn)的開發(fā)人員肯定遇到過亂碼,有時(shí)從Web接收的亂碼,有時(shí)從數(shù)據(jù)庫中讀取的亂碼,有時(shí)是在外部接口中接收的亂碼文件,這些都讓我們困惑不已,甚至是痛苦不堪,看如下代碼:
public class Client58 {
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "漢字";
// 讀取字節(jié)
byte b[] = str.getBytes("UTF-8");
// 重新生成一個(gè)新的字符串
System.out.println(new String(b));
}
}
Java文件是通過IDE工具默認(rèn)創(chuàng)建的,編碼格式是GBK,大家想想看上面的輸出結(jié)果會是什么?可能是亂碼吧?兩個(gè)編碼格式不同。我們暫時(shí)不說結(jié)果,先解釋一下Java中的編碼規(guī)則。Java程序涉及的編碼包括兩部分:
(1)、Java文件編碼:如果我們使用記事本創(chuàng)建一個(gè).java后綴的文件,則文件的編碼格式就是操作系統(tǒng)默認(rèn)的格式。如果是使用IDE工具創(chuàng)建的,如Eclipse,則依賴于IDE的設(shè)置,Eclipse默認(rèn)是操作系統(tǒng)編碼(Windows一般為GBK);
(2)、Class文件編碼:通過javac命令生成的后綴名為.class的文件是UTF-8編碼的UNICODE文件,這在任何操作系統(tǒng)上都是一樣的,只要是.class文件就會使UNICODE格式。需要說明的是,UTF是UNICODE的存儲和傳輸格式,它是為了解決UNICODE的高位占用冗余空間而產(chǎn)生的,使用UTF編碼就意味著字符集使用的是UNICODE.
再回到我們的例子上,getBytes方法會根據(jù)指定的字符集取出字節(jié)數(shù)組(這里按照UNICODE格式來提取),然后程序又通過new String(byte [] bytes)重新生成一個(gè)字符串,來看看String的這個(gè)構(gòu)造函數(shù):通過操作系統(tǒng)默認(rèn)的字符集解碼指定的byte數(shù)組,構(gòu)造一個(gè)新的String,結(jié)果已經(jīng)很清楚了,如果操作系統(tǒng)是UTF-8的話,輸出就是正確的,如果不是,則會是亂碼。由于這里使用的是默認(rèn)編碼GBK,那么輸出的結(jié)果也就是亂碼了。我們再詳細(xì)分解一下運(yùn)行步驟:
步驟1:創(chuàng)建Client58.java文件:該文件的默認(rèn)編碼格式GBK(如果是Eclipse,則可以在屬性中查看到)。
步驟2:編寫代碼(如上);
步驟3:保存,使用javac編譯,注意我們沒有使用"javac -encoding GBK Client58.java" 顯示聲明Java的編碼方式,javac會自動按照操作系統(tǒng)的編碼(GBK)讀取Client58.java文件,然后將其編譯成.class文件。
步驟4:生成.class文件。編譯結(jié)束,生成.class文件,并保存到硬盤上,此時(shí) .class文件使用的UTF-8格式編碼的UNICODE字符集,可以通過javap 命令閱讀class文件,其中" 漢字"變量也已經(jīng)由GBK轉(zhuǎn)變成UNICODE格式了。
步驟5:運(yùn)行main方法,提取"漢字"的字節(jié)數(shù)組。"漢字" 原本是按照UTF-8格式保存的,要再提取出來當(dāng)然沒有任何問題了。
步驟6:重組字符串,讀取操作系統(tǒng)默認(rèn)的編碼GBK,然后重新編碼變量b的所有字節(jié)。問題就在這里產(chǎn)生了:因?yàn)閁NICODE的存儲格式是兩個(gè)字節(jié)表示一個(gè)字符(注意:這里是指UCS-2標(biāo)準(zhǔn)),雖然GBK也是兩個(gè)字節(jié)表示一個(gè)字符,但兩者之間沒有映射關(guān)系,只要做轉(zhuǎn)換只能讀取映射表,不能實(shí)現(xiàn)自動轉(zhuǎn)換----于是JVM就按照默認(rèn)的編碼方式(GBK)讀取了UNICODE的兩個(gè)字節(jié)。
步驟7:輸出亂碼,程序運(yùn)行結(jié)束,問題清楚了,解決方案也隨之產(chǎn)生,方案有兩個(gè)。
步驟8:修改代碼,明確指定編碼即可,代碼如下:
System.out.println(new String(b,"UTF-8"));
步驟9:修改操作系統(tǒng)的編碼方式,各個(gè)操作系統(tǒng)的修改方式不同,不再贅述。
我們可以把字符串讀取字節(jié)的過程看做是數(shù)據(jù)傳輸?shù)男枰?比如網(wǎng)絡(luò)、存儲),而重組字符串則是業(yè)務(wù)邏輯的需求,這樣就可以是亂碼重現(xiàn):通過JDBC讀取的字節(jié)數(shù)組是GBK的,而業(yè)務(wù)邏輯編碼時(shí)采用的是UTF-8,于是亂碼就產(chǎn)生了。對于此類問題,最好的解決辦法就是使用統(tǒng)一的編碼格式,要么都用GBK,要么都用UTF-8,各個(gè)組件、接口、邏輯層、都用UTF-8,拒絕獨(dú)樹一幟的情況。
問題清楚了,我們看看以下代碼:
public class Client58 {
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "漢字";
// 讀取字節(jié)
byte b[] = str.getBytes("GB2312");
// 重新生成一個(gè)新的字符串
System.out.println(new String(b));
}
}
僅僅修改了讀取字節(jié)的編碼方式(修改成了GB2312),結(jié)果會怎樣呢?又或者將其修改成GB18030,結(jié)果又是怎樣的呢?結(jié)果都是"漢字",不是亂碼。這是因?yàn)镚B2312是中文字符集的V1.0版本,GBK是V2.0版本,GB18030是V3.0版本,版本是向下兼容的,只是它們包含的漢字?jǐn)?shù)量不同而已,注意UNICODE可不在這個(gè)序列之內(nèi)。
注意:一個(gè)系統(tǒng)使用統(tǒng)一的編碼。
建議60:性能考慮,數(shù)組是首選
數(shù)組在實(shí)際的系統(tǒng)開發(fā)中用的越來越少了,我們通常只有在閱讀一些開源項(xiàng)目時(shí)才會看到它們的身影,在Java中它確實(shí)沒有List、Set、Map這些集合類用起來方便,但是在基本類型處理方面,數(shù)組還是占優(yōu)勢的,而且集合類的底層也都是通過數(shù)組實(shí)現(xiàn)的,比如對一數(shù)據(jù)集求和這樣的計(jì)算:
//對數(shù)組求和
public static int sum(int datas[]) {
int sum = 0;
for (int i = 0; i < datas.length; i++) {
sum += datas[i];
}
return sum;
}
對一個(gè)int類型 的數(shù)組求和,取出所有數(shù)組元素并相加,此算法中如果是基本類型則使用數(shù)組效率是最高的,使用集合則效率次之。再看使用List求和:
// 對列表求和計(jì)算
public static int sum(List<Integer> datas) {
int sum = 0;
for (int i = 0; i < datas.size(); i++) {
sum += datas.get(i);
}
return sum;
}
注意看sum += datas.get(i);這行代碼,這里其實(shí)已經(jīng)做了一個(gè)拆箱動作,Integer對象通過intValue方法自動轉(zhuǎn)換成了一個(gè)int基本類型,對于性能瀕于臨界的系統(tǒng)來說該方案是比較危險(xiǎn)的,特別是大數(shù)量的時(shí)候,首先,在初始化List數(shù)組時(shí)要進(jìn)行裝箱操作,把一個(gè)int類型包裝成一個(gè)Integer對象,雖然有整型池在,但不在整型池范圍內(nèi)的都會產(chǎn)生一個(gè)新的Integer對象,而且眾所周知,基本類型是在棧內(nèi)存中操作的,而對象是堆內(nèi)存中操作的,棧內(nèi)存的特點(diǎn)是:速度快,容量小;堆內(nèi)存的特點(diǎn)是:速度慢,容量大(從性能上講,基本類型的處理占優(yōu)勢)。其次,在進(jìn)行求和運(yùn)算時(shí)(或者其它遍歷計(jì)算)時(shí)要做拆箱動作,因此無謂的性能消耗也就產(chǎn)生了。在實(shí)際測試中發(fā)現(xiàn):對基本類型進(jìn)行求和運(yùn)算時(shí),數(shù)組的效率是集合的10倍。
注意:性能要求較高的場景中使用數(shù)組代替集合。
建議64:多種最值算法,適時(shí)選擇
對一批數(shù)據(jù)進(jìn)行排序,然后找出其中的最大值或最小值,這是基本的數(shù)據(jù)結(jié)構(gòu)知識。在Java中我們可以通過編寫算法的方式,也可以通過數(shù)組先排序再取值的方式來實(shí)現(xiàn),下面以求最大值為例,解釋一下多種算法:
(1)、自行實(shí)現(xiàn),快速查找最大值
先看看用快速查找法取最大值的算法,代碼如下:
public static int max(int[] data) {
int max = data[0];
for (int i : data) {
max = max > i ? max : i;
}
return max;
}
這是我們經(jīng)常使用的最大值算法,也是速度最快的算法。它不要求排序,只要遍歷一遍數(shù)組即可找出最大值。
(2)、先排序,后取值
對于求最大值,也可以采用先排序后取值的方式,代碼如下:
public static int max(int[] data) {
Arrays.sort(data);
return data[data.length - 1];
}
從效率上講,當(dāng)然是自己寫快速查找法更快一些了,只用遍歷一遍就可以計(jì)算出最大值,但在實(shí)際測試中發(fā)現(xiàn),如果數(shù)組量少于10000,兩個(gè)基本上沒有區(qū)別,但在同一個(gè)毫秒級別里,此時(shí)就可以不用自己寫算法了,直接使用數(shù)組先排序后取值的方式。
如果數(shù)組元素超過10000,就需要依據(jù)實(shí)際情況來考慮:自己實(shí)現(xiàn),可以提高性能;先排序后取值,簡單,通俗易懂。排除性能上的差異,兩者都可以選擇,甚至后者更方便一些,也更容易想到。
現(xiàn)在問題來了,在代碼中為什么先使用data.clone拷貝再排序呢?那是因?yàn)閿?shù)組也是一個(gè)對象,不拷貝就改變了原有的數(shù)組元素的順序嗎?除非數(shù)組元素的順序無關(guān)緊要。那如果要查找僅次于最大值的元素(也就是老二),該如何處理呢?要注意,數(shù)組的元素時(shí)可以重復(fù)的,最大值可能是多個(gè),所以單單一個(gè)排序然后取倒數(shù)第二個(gè)元素時(shí)解決不了問題的。
此時(shí),就需要一個(gè)特殊的排序算法了,先要剔除重復(fù)數(shù)據(jù),然后再排序,當(dāng)然,自己寫算法也可以實(shí)現(xiàn),但是集合類已經(jīng)提供了非常好的方法,要是再使用自己寫算法就顯得有點(diǎn)重復(fù)造輪子了。數(shù)組不能剔除重復(fù)數(shù)據(jù),但Set集合卻是可以的,而且Set的子類TreeSet還能自動排序,代碼如下:
public static int getSecond(Integer[] data) {
//轉(zhuǎn)換為列表
List<Integer> dataList = Arrays.asList(data);
//轉(zhuǎn)換為TreeSet,剔除重復(fù)元素并升序排列
TreeSet<Integer> ts = new TreeSet<Integer>(dataList);
//取得比最大值小的最大值,也就是老二了
return ts.lower(ts.last());
}
剔除重復(fù)元素并升序排列,這都是由TreeSet類實(shí)現(xiàn)的,然后可再使用lower方法尋找小于最大值的值,大家看,上面的程序非常簡單吧?那如果是我們自己編寫代碼會怎么樣呢?那至少要遍歷數(shù)組兩遍才能計(jì)算出老二的值,代碼復(fù)雜度將大大提升。因此在實(shí)際應(yīng)用中求最值,包括最大值、最小值、倒數(shù)第二小值等,使用集合是最簡單的方式,當(dāng)然從性能方面來考慮,數(shù)組才是最好的選擇。
注意:最值計(jì)算時(shí)使用集合最簡單,使用數(shù)組性能最優(yōu)。
建議82:由點(diǎn)及面,集合大家族總結(jié)
Java中的集合類實(shí)在是太豐富了,有常用的ArrayList、HashMap,也有不常用的Stack、Queue,有線程安全的Vector、HashTable,也有線程不安全的LinkedList、TreeMap,有阻塞式的ArrayBlockingQueue,也有非阻塞式的PriorityQueue等,整個(gè)集合大家族非常龐大,可以劃分以下幾類:
(1)、List:實(shí)現(xiàn)List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一個(gè)動態(tài)數(shù)組,LinkedList是一個(gè)雙向鏈表,Vector是一個(gè)線程安全的動態(tài)數(shù)組,Stack是一個(gè)對象棧,遵循先進(jìn)后出的原則。
(2)、Set:Set是不包含重復(fù)元素的集合,其主要實(shí)現(xiàn)類有:EnumSet、HashSet、TreeSet,其中EnumSet是枚舉類型專用Set,所有元素都是枚舉類型;HashSet是以哈希碼決定其元素位置的Set,其原理與HashMap相似,它提供快速的插入和查找方法;TreeSet是一個(gè)自動排序的Set,它實(shí)現(xiàn)了SortedSet接口。
(3)、Map:Map是一個(gè)大家族,他可以分為排序Map和非排序Map,排序Map主要是TreeMap類,他根據(jù)key值進(jìn)行自動排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子類,它的主要用途是從Property文件中加載數(shù)據(jù),并提供方便的操作,EnumMap則是要求其Key必須是某一個(gè)枚舉類型。
Map中還有一個(gè)WeakHashMap類需要說明,它是一個(gè)采用弱鍵方式實(shí)現(xiàn)的Map類,它的特點(diǎn)是:WeakHashMap對象的存在并不會阻止垃圾回收器對鍵值對的回收,也就是說使用WeakHashMap裝載數(shù)據(jù)不用擔(dān)心內(nèi)存溢出的問題,GC會自動刪除不用的鍵值對,這是好事。但也存在一個(gè)嚴(yán)重的問題:GC是靜悄悄的回收的(何時(shí)回收,God,Knows!)我們的程序無法知曉該動作,存在著重大的隱患。(4)、Queue:對列,它分為兩類,一類是阻塞式隊(duì)列,隊(duì)列滿了以后再插入元素會拋出異常,主要包括:ArrayBlockingQueue、PriorityQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一個(gè)以數(shù)組方式實(shí)現(xiàn)的有界阻塞隊(duì)列;另一類是非阻塞隊(duì)列,無邊界的,只要內(nèi)存允許,都可以持續(xù)追加元素,我們經(jīng)常使用的是PriorityQuene類。
還有一種隊(duì)列,是雙端隊(duì)列,支持在頭、尾兩端插入和移除元素,它的主要實(shí)現(xiàn)類是:ArrayDeque、LinkedBlockingDeque、LinkedList。(5)、數(shù)組:數(shù)組與集合的最大區(qū)別就是數(shù)組能夠容納基本類型,而集合就不行,更重要的一點(diǎn)就是所有的集合底層存儲的都是數(shù)組。
(6)、工具類:數(shù)組的工具類是java.util.Arrays和java.lang.reflect.Array,集合的工具類是java.util.Collections,有了這兩個(gè)工具類,操作數(shù)組和集合就會易如反掌,得心應(yīng)手。
(7)、擴(kuò)展類:集合類當(dāng)然可以自行擴(kuò)展了,想寫一個(gè)自己的List?沒問題,但最好的辦法還是"拿來主義",可以使用Apache的common-collections擴(kuò)展包,也可以使用Google的google-collections擴(kuò)展包,這些足以應(yīng)對我們的開發(fā)需要。
建議83:推薦使用枚舉定義常量
常量聲明是每一個(gè)項(xiàng)目都不可或缺的,在Java1.5之前,我們只有兩種方式的聲明:類常量和接口常量,若在項(xiàng)目中使用的是Java1.5之前的版本,基本上都是如此定義的。不過,在1.5版本以后有了改進(jìn),即新增了一種常量聲明方式:枚舉聲明常量,看如下代碼:
enum Season {
Spring, Summer, Autumn, Winter;
}
這是一個(gè)簡單的枚舉常量命名,清晰又簡單。順便提一句,JLS(Java Language Specification,Java語言規(guī)范)提倡枚舉項(xiàng)全部大寫,字母之間用下劃線分割,這也是從常量的角度考慮的(當(dāng)然,使用類似類名的命名方式也是比較友好的)。
那么枚舉常量與我們經(jīng)常使用的類常量和靜態(tài)常量相比有什么優(yōu)勢?問得好,枚舉的優(yōu)點(diǎn)主要表現(xiàn)在四個(gè)方面:
1.枚舉常量簡單:簡不簡單,我們來對比一下兩者的定義和使用情況就知道了。先把Season枚舉翻寫成接口常量,代碼如下:
interface Season {
int SPRING = 0;
int SUMMER = 1;
int AUTUMN = 2;
int WINTER = 3;
}
此處定義了春夏秋冬四個(gè)季節(jié),類型都是int,這與Season枚舉的排序值是相同的。首先對比一下兩者的定義,枚舉常量只需定義每個(gè)枚舉項(xiàng),不需要定義枚舉值,而接口常量(或類常量)則必須定義值,否則編譯不通過,即使我們不需要關(guān)注其值是多少也必須定義;其次,雖然兩者被引用的方式相同(都是 “類名 . 屬性”,如Season.SPRING),但是枚舉表示的是一個(gè)枚舉項(xiàng),字面含義是春天,而接口常量確是一個(gè)int類型,雖然其字面含義也是春天,但在運(yùn)算中我們勢必要關(guān)注其int值。
2.枚舉常量屬于穩(wěn)態(tài)型:例如我們要描述一下春夏秋冬是什么樣子,使用接口常量應(yīng)該是這樣寫。
public void describe(int s) {
// s變量不能超越邊界,校驗(yàn)條件
if (s >= 0 && s < 4) {
switch (s) {
case Season.SPRING:
System.out.println("this is spring");
break;
case Season.SUMMER:
System.out.println("this is summer");
break;
......
}
}
}
很簡單,先使用switch語句判斷哪一個(gè)是常量,然后輸出。但問題是我們得對輸入值進(jìn)行檢查,確定是否越界,如果常量非常龐大,校驗(yàn)輸入就成了一件非常麻煩的事情,但這是一個(gè)不可逃避的過程,特別是如果我們的校驗(yàn)條件不嚴(yán)格,雖然編譯能照樣通過,但是運(yùn)行期就會產(chǎn)生無法預(yù)知的后果。
我們再來看看枚舉常量是否能夠避免校驗(yàn)的問題,代碼如下:
public void describe(Season s){
switch(s){
case Spring:
System.out.println("this is "+Season.Spring);
break;
case Summer:
System.out.println("this is summer"+Season.Summer);
break;
......
}
}
不用校驗(yàn),已經(jīng)限定了是Season枚舉,所以只能是Season類的四個(gè)實(shí)例,即春夏秋冬4個(gè)枚舉項(xiàng),想輸入一個(gè)int類型或其它類型?門都沒有!這是我們最看重枚舉的地方:在編譯期間限定類型,不允許發(fā)生越界的情況。
3.枚舉具有內(nèi)置方法:有一個(gè)簡單的問題:如果要列出所有的季節(jié)常量,如何實(shí)現(xiàn)呢?接口常量或類常量可以通過反射來實(shí)現(xiàn),這沒錯,只是雖然能實(shí)現(xiàn),但會非常繁瑣,大家可以自己寫一個(gè)反射類實(shí)現(xiàn)此功能(當(dāng)然,一個(gè)一個(gè)地動手打印出輸出常量,也可以算是列出)。對于此類問題可以非常簡單的解決,代碼如下:
public void query() {
for (Season s : Season.values()) {
System.out.println(s);
}
}
通過values方法獲得所有的枚舉項(xiàng),然后打印出來即可。如此簡單,得益于枚舉內(nèi)置的方法,每個(gè)枚舉都是java.lang.Enum的子類,該基類提供了諸如獲得排序值的ordinal方法、compareTo比較方法等,大大簡化了常量的訪問。
4.枚舉可以自定義的方法:這一點(diǎn)似乎并不是枚舉的優(yōu)點(diǎn),類常量也可以有自己的方法呀,但關(guān)鍵是枚舉常量不僅可以定義靜態(tài)方法,還可以定義非靜態(tài)方法,而且還能夠從根本上杜絕常量類被實(shí)例化。比如我們要在常量定義中獲得最舒服季節(jié)的方法,使用常量枚舉的代碼如下:
enum Season {
Spring, Summer, Autumn, Winter;
public static Season getComfortableSeason(){
return Spring;
}
}
我們知道,每個(gè)枚舉項(xiàng)都是該枚舉的一個(gè)實(shí)例,對于我們的例子來說,也就表示Spring其實(shí)是Season的一個(gè)實(shí)例,Summer也是其中一個(gè)實(shí)例,那我們在枚舉中定義的靜態(tài)方法既可以在類(也就是枚舉Season)中引用,也可以在實(shí)例(也就是枚舉項(xiàng)Spring、Summer、Autumn、Winter)中引用,看如下代碼:
public static void main(String[] args) {
System.out.println("The most comfortable season is "+Season.getComfortableSeason());
}
那如果使用類常量要如何實(shí)現(xiàn)呢?代碼如下:
class Season {
public final static int SPRING = 0;
public final static int SUMMER = 1;
public final static int AUTUMN = 2;
public final static int WINTER = 3;
public static int getComfortableSeason(){
return SPRING;
}
}
想想看,我們怎么才能打印出"The most comfortable season is Spring" 這句話呢?除了使用switch和if判斷之外沒有其它辦法了。
雖然枚舉在很多方面比接口常量和類常量好用,但是有一點(diǎn)它是比不上接口常量和類常量的,那就是繼承,枚舉類型是不能繼承的,也就是說一個(gè)枚舉常量定義完畢后,除非修改重構(gòu),否則無法做擴(kuò)展,而接口常量和類常量則可以通過繼承進(jìn)行擴(kuò)展。但是,一般常量在項(xiàng)目構(gòu)建時(shí)就定義完畢了,很少會出現(xiàn)必須通過擴(kuò)展才能實(shí)現(xiàn)業(yè)務(wù)邏輯的場景。
注意: 在項(xiàng)目中推薦使用枚舉常量代替接口常量或類常量。
建議88:用枚舉實(shí)現(xiàn)工廠方法模式更簡潔
工廠方法模式(Factory Method Pattern)是" 創(chuàng)建對象的接口,讓子類決定實(shí)例化哪一個(gè)類,并使一個(gè)類的實(shí)例化延遲到其它子類"。工廠方法模式在我們的開發(fā)中經(jīng)常會用到。下面以汽車制造為例,看看一般的工廠方法模式是如何實(shí)現(xiàn)的,代碼如下:
//抽象產(chǎn)品
interface Car{
}
//具體產(chǎn)品類
class FordCar implements Car{
}
//具體產(chǎn)品類
class BuickCar implements Car{
}
//工廠類
class CarFactory{
//生產(chǎn)汽車
public static Car createCar(Class<? extends Car> c){
try {
return c.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
這是最原始的工廠方法模式,有兩個(gè)產(chǎn)品:福特汽車和別克汽車,然后通過工廠方法模式來生產(chǎn)。有了工廠方法模式,我們就不用關(guān)心一輛車具體是怎么生成的了,只要告訴工廠" 給我生產(chǎn)一輛福特汽車 "就可以了,下面是產(chǎn)出一輛福特汽車時(shí)客戶端的代碼:
public static void main(String[] args) {
//生產(chǎn)車輛
Car car = CarFactory.createCar(FordCar.class);
}
這就是我們經(jīng)常使用的工廠方法模式,但經(jīng)常使用并不代表就是最優(yōu)秀、最簡潔的。此處再介紹一種通過枚舉實(shí)現(xiàn)工廠方法模式的方案,誰優(yōu)誰劣你自行評價(jià)。枚舉實(shí)現(xiàn)工廠方法模式有兩種方法:
(1)、枚舉非靜態(tài)方法實(shí)現(xiàn)工廠方法模式
我們知道每個(gè)枚舉項(xiàng)都是該枚舉的實(shí)例對象,那是不是定義一個(gè)方法可以生成每個(gè)枚舉項(xiàng)對應(yīng)產(chǎn)品來實(shí)現(xiàn)此模式呢?代碼如下:
enum CarFactory {
// 定義生產(chǎn)類能生產(chǎn)汽車的類型
FordCar, BuickCar;
// 生產(chǎn)汽車
public Car create() {
switch (this) {
case FordCar:
return new FordCar();
case BuickCar:
return new BuickCar();
default:
throw new AssertionError("無效參數(shù)");
}
}
}
create是一個(gè)非靜態(tài)方法,也就是只有通過FordCar、BuickCar枚舉項(xiàng)才能訪問。采用這種方式實(shí)現(xiàn)工廠方法模式時(shí),客戶端要生產(chǎn)一輛汽車就很簡單了,代碼如下:
public static void main(String[] args) {
// 生產(chǎn)車輛
Car car = CarFactory.BuickCar.create();
}
(2)、通過抽象方法生成產(chǎn)品
枚舉類型雖然不能繼承,但是可以用abstract修飾其方法,此時(shí)就表示該枚舉是一個(gè)抽象枚舉,需要每個(gè)枚舉項(xiàng)自行實(shí)現(xiàn)該方法,也就是說枚舉項(xiàng)的類型是該枚舉的一個(gè)子類,我們倆看代碼:
enum CarFactory {
// 定義生產(chǎn)類能生產(chǎn)汽車的類型
FordCar{
public Car create(){
return new FordCar();
}
},
BuickCar{
public Car create(){
return new BuickCar();
}
};
//抽象生產(chǎn)方法
public abstract Car create();
}
首先定義一個(gè)抽象制造方法create,然后每個(gè)枚舉項(xiàng)自行實(shí)現(xiàn),這種方式編譯后會產(chǎn)生CarFactory的匿名子類,因?yàn)槊總€(gè)枚舉項(xiàng)都要實(shí)現(xiàn)create抽象方法。客戶端調(diào)用與上一個(gè)方案相同,不再贅述。
大家可能會問,為什么要使用枚舉類型的工廠方法模式呢?那是因?yàn)槭褂妹杜e類型的工廠方法模式有以下三個(gè)優(yōu)點(diǎn):
- 避免錯誤調(diào)用的發(fā)生:一般工廠方法模式中的生產(chǎn)方法(也就是createCar方法),可以接收三種類型的參數(shù):類型參數(shù)(如我們的例子)、String參數(shù)(生產(chǎn)方法中判斷String參數(shù)是需要生產(chǎn)什么產(chǎn)品)、int參數(shù)(根據(jù)int值判斷需要生產(chǎn)什么類型的的產(chǎn)品),這三種參數(shù)都是寬泛的數(shù)據(jù)類型,很容易發(fā)生錯誤(比如邊界問題、null值問題),而且出現(xiàn)這類錯誤編譯器還不會報(bào)警,例如:
public static void main(String[] args) {
// 生產(chǎn)車輛
Car car = CarFactory.createCar(Car.class);
}
Car是一個(gè)接口,完全合乎createCar的要求,所以它在編譯時(shí)不會報(bào)任何錯誤,但一運(yùn)行就會報(bào)出InstantiationException異常,而使用枚舉類型的工廠方法模式就不存在該問題了,不需要傳遞任何參數(shù),只需要選擇好生產(chǎn)什么類型的產(chǎn)品即可。
性能好,使用簡潔:枚舉類型的計(jì)算時(shí)以int類型的計(jì)算為基礎(chǔ)的,這是最基本的操作,性能當(dāng)然會快,至于使用便捷,注意看客戶端的調(diào)用,代碼的字面意思就是" 汽車工廠,我要一輛別克汽車,趕快生產(chǎn)"。
降低類間耦合:不管生產(chǎn)方法接收的是Class、String還是int的參數(shù),都會成為客戶端類的負(fù)擔(dān),這些類并不是客戶端需要的,而是因?yàn)楣S方法的限制必須輸入的,例如Class參數(shù),對客戶端main方法來說,他需要傳遞一個(gè)FordCar.class參數(shù)才能生產(chǎn)一輛福特汽車,除了在create方法中傳遞參數(shù)外,業(yè)務(wù)類不需要改Car的實(shí)現(xiàn)類。這嚴(yán)重違背了迪米特原則(Law of Demeter 簡稱LoD),也就是最少知識原則:一個(gè)對象應(yīng)該對其它對象有最少的了解。
而枚舉類型的工廠方法就沒有這種問題了,它只需要依賴工廠類就可以生產(chǎn)一輛符合接口的汽車,完全可以無視具體汽車類的存在。
建議93:Java的泛型是可以擦除的
Java泛型(Generic) 的引入加強(qiáng)了參數(shù)類型的安全性,減少了類型的轉(zhuǎn)換,它與C++中的模板(Temeplates) 比較類似,但是有一點(diǎn)不同的是:Java的泛型在編譯器有效,在運(yùn)行期被刪除,也就是說所有的泛型參數(shù)類型在編譯后會被清除掉,我們來看一個(gè)例子,代碼如下:
public class Foo {
//arrayMethod接收數(shù)組參數(shù),并進(jìn)行重載
public void arrayMethod(String[] intArray) {
}
public void arrayMethod(Integer[] intArray) {
}
//listMethod接收泛型List參數(shù),并進(jìn)行重載
public void listMethod(List<String> stringList) {
}
public void listMethod(List<Integer> intList) {
}
}
程序很簡單,編寫了4個(gè)方法,arrayMethod方法接收String數(shù)組和Integer數(shù)組,這是一個(gè)典型的重載,listMethod接收元素類型為String和Integer的list變量。現(xiàn)在的問題是,這段程序是否能編譯?如果不能?問題出在什么地方?
事實(shí)上,這段程序時(shí)無法編譯的,編譯時(shí)報(bào)錯信息如下:
這段錯誤的意思:簡單的的說就是方法簽名重復(fù),其實(shí)就是說listMethod(List<Integer> intList)方法在編譯時(shí)擦除類型后是listMethod(List<E> intList)與另一個(gè)方法重復(fù)。這就是Java泛型擦除引起的問題:在編譯后所有的泛型類型都會做相應(yīng)的轉(zhuǎn)化。轉(zhuǎn)換規(guī)則如下:
- List<String>、List<Integer>、List<T>擦除后的類型為List
- List<String>[] 擦除后的類型為List[].
- List<? extends E> 、List<? super E> 擦除后的類型為List<E>.
- List<T extends Serializable & Cloneable >擦除后的類型為List< Serializable>.
明白了這些規(guī)則,再看如下代碼:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abc");
String str = list.get(0);
}
進(jìn)過編譯后的擦除處理,上面的代碼和下面的程序時(shí)一致的:
public static void main(String[] args) {
List list = new ArrayList();
list.add("abc");
String str = (String) list.get(0);
}
Java編譯后字節(jié)碼中已經(jīng)沒有泛型的任何信息了,也就是說一個(gè)泛型類和一個(gè)普通類在經(jīng)過編譯后都指向了同一字節(jié)碼,比如Foo<T>類,經(jīng)過編譯后將只有一份Foo.class類,不管是Foo<String>還是Foo<Integer>引用的都是同一字節(jié)碼。Java之所以如此處理,有兩個(gè)原因:
- 避免JVM的大換血。C++泛型生命期延續(xù)到了運(yùn)行期,而Java是在編譯期擦除掉的,我們想想,如果JVM也把泛型類型延續(xù)到運(yùn)行期,那么JVM就需要進(jìn)行大量的重構(gòu)工作了。
- 版本兼容:在編譯期擦除可以更好的支持原生類型(Raw Type),在Java1.5或1.6...平臺上,即使聲明一個(gè)List這樣的原生類型也是可以正常編譯通過的,只是會產(chǎn)生警告信息而已。
明白了Java泛型是類型擦除的,我們就可以解釋類似如下的問題了:
1.泛型的class對象是相同的:每個(gè)類都有一個(gè)class屬性,泛型化不會改變class屬性的返回值,例如:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list.getClass()==list2.getClass());
}
以上代碼返回true,原因很簡單,List<String>和List<Integer>擦除后的類型都是List,沒有任何區(qū)別。
2.泛型數(shù)組初始化時(shí)不能聲明泛型,如下代碼編譯時(shí)通不過:
List<String>[] listArray = new List<String>[];
原因很簡單,可以聲明一個(gè)帶有泛型參數(shù)的數(shù)組,但不能初始化該數(shù)組,因?yàn)閳?zhí)行了類型擦除操作,List<Object>[]與List<String>[] 就是同一回事了,編譯器拒絕如此聲明。
3.instanceof不允許存在泛型參數(shù):以下代碼不能通過編譯,原因一樣,泛型類型被擦除了:
List<String> list = new ArrayList<String>();
System.out.println(list instanceof List<String>);
建議98:建議的采用順序是List中泛型順序依次為T、?、Object
List<T>、List<?>、List<Object>這三者都可以容納所有的對象,但使用的順序應(yīng)該是首選List<T>,次之List<?>,最后選擇List<Object>,原因如下:
(1)、List<T>是確定的某一個(gè)類型
List<T>表示的是List集合中的元素都為T類型,具體類型在運(yùn)行期決定;List<?>表示的是任意類型,與List<T>類似,而List<Object>則表示List集合中的所有元素為Object類型,因?yàn)镺bject是所有類的父類,所以List<Object>也可以容納所有的類類型,從這一字面意義上分析,List<T>更符合習(xí)慣:編碼者知道它是某一個(gè)類型,只是在運(yùn)行期才確定而已。
(2)List<T>可以進(jìn)行讀寫操作
List<T>可以進(jìn)行諸如add,remove等操作,因?yàn)樗念愋褪枪潭ǖ腡類型,在編碼期不需要進(jìn)行任何的轉(zhuǎn)型操作。
List<T>是只讀類型的,不能進(jìn)行增加、修改操作,因?yàn)榫幾g器不知道List中容納的是什么類型的元素,也就無法校驗(yàn)類型是否安全了,而且List<?>讀取出的元素都是Object類型的,需要主動轉(zhuǎn)型,所以它經(jīng)常用于泛型方法的返回值。注意List<?>雖然無法增加,修改元素,但是卻可以刪除元素,比如執(zhí)行remove、clear等方法,那是因?yàn)樗膭h除動作與泛型類型無關(guān)。
List<Object> 也可以讀寫操作,但是它執(zhí)行寫入操作時(shí)需要向上轉(zhuǎn)型(Up cast),在讀取數(shù)據(jù)的時(shí)候需要向下轉(zhuǎn)型,而此時(shí)已經(jīng)失去了泛型存在的意義了。
打個(gè)比方,有一個(gè)籃子用來容納物品,比如西瓜,番茄等.List<?>的意思是說,“嘿,我這里有一個(gè)籃子,可以容納固定類別的東西,比如西瓜,番茄等”。List<?>的意思是說:“嘿,我有一個(gè)籃子,我可以容納任何東西,只要是你想得到的”。而List<Object>就更有意思了,它說" 嘿,我也有一個(gè)籃子,我可以容納所有物質(zhì),只要你認(rèn)為是物質(zhì)的東西都可以容納進(jìn)來 "。
推而廣之,Dao<T>應(yīng)該比Dao<?>、Dao<Object>更先采用,Desc<Person>則比Desc<?>、Desc<Object>更優(yōu)先采用。
建議101:注意Class類的特殊性
Java語言是先把Java源文件編譯成后綴為class的字節(jié)碼文件,然后再通過ClassLoader機(jī)制把這些類文件加載到內(nèi)存中,最后生成實(shí)例執(zhí)行的,這是Java處理的基本機(jī)制,但是加載到內(nèi)存中的數(shù)據(jù)的如何描述一個(gè)類的呢?比如在Dog.class文件中定義一個(gè)Dog類,那它在內(nèi)存中是如何展現(xiàn)的呢?
Java使用一個(gè)元類(MetaClass)來描述加載到內(nèi)存中的類數(shù)據(jù),這就是Class類,它是一個(gè)描述類的類對象,比如Dog.class文件加載到內(nèi)存中后就會有一個(gè)class的實(shí)例對象描述之。因?yàn)槭荂lass類是“類中類”,也就有預(yù)示著它有很多特殊的地方:
- 1.無構(gòu)造函數(shù):Java中的類一般都有構(gòu)造函數(shù),用于創(chuàng)建實(shí)例對象,但是Class類卻沒有構(gòu)造函數(shù),不能實(shí)例化,Class對象是在加載類時(shí)由Java虛擬機(jī)通過調(diào)用類加載器中的difineClass方法自動構(gòu)造的。
- 2.可以描述基本類型:雖然8個(gè)基本類型在JVM中并不是一個(gè)對象,它們一般存在于棧內(nèi)存中,但是Class類仍然可以描述它們,例如可以使用int.class表示int類型的類對象。
- 3.其對象都是單例模式:一個(gè)Class的實(shí)例對象描述一個(gè)類,并且只描述一個(gè)類,反過來也成立。一個(gè)類只有一個(gè)Class實(shí)例對象,如下代碼返回的結(jié)果都為true:
// 類的屬性class所引用的對象與實(shí)例對象的getClass返回值相同
boolean b1=String.class.equals(new String().getClass());
boolean b2="ABC".getClass().equals(String.class);
// class實(shí)例對象不區(qū)分泛型
boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());
Class類是Java的反射入口,只有在獲得了一個(gè)類的描述對象后才能動態(tài)的加載、調(diào)用,一般獲得一個(gè)Class對象有三種途徑:
- 類屬性方式:如String.class
- 對象的getClass方法,如new String().getClass()
- forName方法加載:如Class.forName(" java.lang.String")
獲得了Class對象后,就可以通過getAnnotations()獲得注解,通過getMethods()獲得方法,通過getConstructors()獲得構(gòu)造函數(shù)等,這位后續(xù)的反射代碼鋪平了道路。
建議106:動態(tài)代理可以使代理模式更加靈活
Java的反射框架提供了動態(tài)代理(Dynamic Proxy)機(jī)制,允許在運(yùn)行期對目標(biāo)類生成代理,避免重復(fù)開發(fā)。我們知道一個(gè)靜態(tài)代理是通過主題角色(Proxy)和具體主題角色(Real Subject)共同實(shí)現(xiàn)主題角色(Subject)的邏輯的,只是代理角色把相關(guān)的執(zhí)行邏輯委托給了具體角色而已,一個(gè)簡單的靜態(tài)代理如下所示:
interface Subject {
// 定義一個(gè)方法
public void request();
}
// 具體主題角色
class RealSubject implements Subject {
// 實(shí)現(xiàn)方法
@Override
public void request() {
// 實(shí)現(xiàn)具體業(yè)務(wù)邏輯
}
}
class Proxy implements Subject {
// 要代理那個(gè)實(shí)現(xiàn)類
private Subject subject = null;
// 默認(rèn)被代理者
public Proxy() {
subject = new RealSubject();
}
// 通過構(gòu)造函數(shù)傳遞被代理者
public Proxy(Subject _subject) {
subject = _subject;
}
@Override
public void request() {
before();
subject.request();
after();
}
// 預(yù)處理
private void after() {
// doSomething
}
// 善后處理
private void before() {
// doSomething
}
}
這是一個(gè)簡單的靜態(tài)代理。Java還提供了java.lang.reflect.Proxy用于實(shí)現(xiàn)動態(tài)代理:只要提供一個(gè)抽象主題角色和具體主題角色,就可以動態(tài)實(shí)現(xiàn)其邏輯的,其實(shí)例代碼如下:
interface Subject {
// 定義一個(gè)方法
public void request();
}
// 具體主題角色
class RealSubject implements Subject {
// 實(shí)現(xiàn)方法
@Override
public void request() {
// 實(shí)現(xiàn)具體業(yè)務(wù)邏輯
}
}
class SubjectHandler implements InvocationHandler {
// 被代理的對象
private Subject subject;
public SubjectHandler(Subject _subject) {
subject = _subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 預(yù)處理
System.out.println("預(yù)處理...");
//直接調(diào)用被代理的方法
Object obj = method.invoke(subject, args);
// 后處理
System.out.println("后處理...");
return obj;
}
}
注意這里沒有代理主題角色,取而代之的是SubjectHandler 作為主要的邏輯委托處理,其中invoke方法是接口InvocationHandler定義必須實(shí)現(xiàn)的,它完成了對真實(shí)方法的調(diào)用。
我們來詳細(xì)解釋一下InvocationHandler接口,動態(tài)代理是根據(jù)被代理的接口生成的所有方法的,也就是說給定一個(gè)或多個(gè)接口,動態(tài)代理會宣稱“我已經(jīng)實(shí)現(xiàn)該接口下的所有方法了”,那大家想想看,動態(tài)代理是怎么才能實(shí)現(xiàn)接口中的方法呢?在默認(rèn)情況下所有方法的返回值都是空的,是的,雖然代理已經(jīng)實(shí)現(xiàn)了它,但是沒有任何的邏輯含義,那怎么辦?好辦,通過InvocationHandler接口的實(shí)現(xiàn)類來實(shí)現(xiàn),所有的方法都是由該Handler進(jìn)行處理的,即所有被代理的方法都由InvocationHandler接管實(shí)際的處理任務(wù)。
我們開看看動態(tài)代理的場景,代碼如下:
public static void main(String[] args) {
//具體主題角色,也就是被代理類
Subject subject = new RealSubject();
//代理實(shí)例的處理Handler
InvocationHandler handler =new SubjectHandler(subject);
//當(dāng)前加載器
ClassLoader cl = subject.getClass().getClassLoader();
//動態(tài)代理
Subject proxy = (Subject) Proxy.newProxyInstance(cl,subject.getClass().getInterfaces(),handler);
//執(zhí)行具體主題角色方法
proxy.request();
}
此時(shí)就實(shí)現(xiàn)了,不用顯式創(chuàng)建代理類即實(shí)現(xiàn)代理的功能,例如可以在被代理的角色執(zhí)行前進(jìn)行權(quán)限判斷,或者執(zhí)行后進(jìn)行數(shù)據(jù)校驗(yàn)。
動態(tài)代理很容易實(shí)現(xiàn)通用的代理類,只要在InvocationHandler的invoke方法中讀取持久化的數(shù)據(jù)即可實(shí)現(xiàn),而且還能實(shí)現(xiàn)動態(tài)切入的效果,這也是AOP(Aspect Oriented Programming)變成理念。
建議110:提倡異常封裝
Java語言的異常處理機(jī)制可以去確保程序的健壯性,提高系統(tǒng)的可用率,但是Java API提供的異常都是比較低級的(這里的低級是指 " 低級別的 " 異常),只有開發(fā)人員才能看的懂,才明白發(fā)生了什么問題。而對于終端用戶來說,這些異?;旧暇褪翘鞎c業(yè)務(wù)無關(guān),是純計(jì)算機(jī)語言的描述,那該怎么辦?這就需要我們對異常進(jìn)行封裝了。異常封裝有三方面的優(yōu)點(diǎn):
(1)、提高系統(tǒng)的友好性
例如,打開一個(gè)文件,如果文件不存在,則回報(bào)FileNotFoundException異常,如果該方法的編寫者不做任何處理,直接拋到上層,則會降低系統(tǒng)的友好性,代碼如下所示:
public static void doStuff() throws FileNotFoundException {
InputStream is = new FileInputStream("無效文件.txt");
/* 文件操作 */
}
此時(shí)doStuff的友好性極差,出現(xiàn)異常時(shí)(如果文件不存在),該方法直接把FileNotFoundException異常拋到上層應(yīng)用中(或者是最終用戶),而上層應(yīng)用(或用戶要么自己處理),要么接著拋,最終的結(jié)果就是讓用戶面對著" 天書 " 式的文字發(fā)呆,用戶不知道這是什么問題,只是知道系統(tǒng)告訴他" 哦,我出錯了,什么錯誤?你自己看著辦吧 "。
解決辦法就是封裝異常,可以把異常的閱讀者分為兩類:開發(fā)人員和用戶。開發(fā)人員查找問題,需要打印出堆棧信息,而用戶則需要了解具體的業(yè)務(wù)原因,比如文件太大、不能同時(shí)編寫文件等,代碼如下:
public static void doStuff2() throws MyBussinessException{
try {
InputStream is = new FileInputStream("無效文件.txt");
} catch (FileNotFoundException e) {
//方便開發(fā)人員和維護(hù)人員而設(shè)置的異常信息
e.printStackTrace();
//拋出業(yè)務(wù)異常
throw new MyBussinessException();
}
/* 文件操作 */
}
(2)、提高系統(tǒng)的可維護(hù)性
看如下代碼:
public void doStuff3(){
try{
//doSomething
}catch(Exception e){
e.printStackTrace();
}
}
這是大家很容易犯的錯誤,拋出異常是吧?分類處理多麻煩,就寫一個(gè)catch塊來處理所有的異常吧,而且還信誓旦旦的說" JVM會打印出棧中的錯誤信息 ",雖然這沒錯,但是該信息只有開發(fā)人員自己看的懂,維護(hù)人員看到這段異常時(shí)基本上無法處理,因?yàn)樾枰酱a邏輯中去分析問題。
正確的做法是對異常進(jìn)行分類處理,并進(jìn)行封裝輸出,代碼如下:
public void doStuff4(){
try{
//doSomething
}catch(FileNotFoundException e){
log.info("文件未找到,使用默認(rèn)配置文件....");
e.printStackTrace();
}catch(SecurityException e1){
log.info(" 無權(quán)訪問,可能原因是......");
e1.printStackTrace();
}
}
如此包裝后,維護(hù)人員看到這樣的異常就有了初步的判斷,或者檢查配置,或者初始化環(huán)境,不需要直接到代碼層級去分析了。
(3)、解決Java異常機(jī)制自身的缺陷
Java中的異常一次只能拋出一個(gè),比如doStuff方法有兩個(gè)邏輯代碼片段,如果在第一個(gè)邏輯片段中拋出異常,則第二個(gè)邏輯片段就不再執(zhí)行了,也就無法拋出第二個(gè)異常了,現(xiàn)在的問題是:如何才能一次拋出兩個(gè)(或多個(gè))異常呢?
其實(shí),使用自行封裝的異??梢越鉀Q該問題,代碼如下:
class MyException extends Exception {
// 容納所有的異常
private List<Throwable> causes = new ArrayList<Throwable>();
// 構(gòu)造函數(shù),傳遞一個(gè)異常列表
public MyException(List<? extends Throwable> _causes) {
causes.addAll(_causes);
}
// 讀取所有的異常
public List<Throwable> getExceptions() {
return causes;
}
}
MyException異常只是一個(gè)異常容器,可以容納多個(gè)異常,但它本身并不代表任何異常含義,它所解決的是一次拋出多個(gè)異常的問題,具體調(diào)用如下:
public void doStuff() throws MyException {
List<Throwable> list = new ArrayList<Throwable>();
// 第一個(gè)邏輯片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
// 第二個(gè)邏輯片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
// 檢查是否有必要拋出異常
if (list.size() > 0) {
throw new MyException(list);
}
}
這樣一來,DoStuff方法的調(diào)用者就可以一次獲得多個(gè)異常了,也能夠?yàn)橛脩籼峁┩暾睦馇闆r說明。可能有人會問:這種情況會出現(xiàn)嗎?怎么回要求一個(gè)方法拋出多個(gè)異常呢?
絕對有可能出現(xiàn),例如Web界面注冊時(shí),展現(xiàn)層依次把User對象傳遞到邏輯層,Register方法需要對各個(gè)Field進(jìn)行校驗(yàn)并注冊,例如用戶名不能重復(fù),密碼必須符合密碼策略等,不要出現(xiàn)用戶第一次提交時(shí)系統(tǒng)顯示" 用戶名重復(fù) ",在用戶修改用戶名再次提交后,系統(tǒng)又提示" 密碼長度小于6位 " 的情況,這種操作模式下的用戶體驗(yàn)非常糟糕,最好的解決辦法就是異常封裝,建立異常容器,一次性地對User對象進(jìn)行校驗(yàn),然后返回所有的異常。
建議114:不要在構(gòu)造函數(shù)中拋出異常
Java異常的機(jī)制有三種:
- Error類及其子類表示的是錯誤,它是不需要程序員處理也不能處理的異常,比如VirtualMachineError虛擬機(jī)錯誤,ThreadDeath線程僵死等。
- RunTimeException類及其子類表示的是非受檢異常,是系統(tǒng)可能會拋出的異常,程序員可以去處理,也可以不處理,最經(jīng)典的就是NullPointException空指針異常和IndexOutOfBoundsException越界異常。
- Exception類及其子類(不包含非受檢異常),表示的是受檢異常,這是程序員必須處理的異常,不處理則程序不能通過編譯,比如IOException表示的是I/O異常,SQLException表示的數(shù)據(jù)庫訪問異常。
我們知道,一個(gè)對象的創(chuàng)建過程經(jīng)過內(nèi)存分配,靜態(tài)代碼初始化、構(gòu)造函數(shù)執(zhí)行等過程,對象生成的關(guān)鍵步驟是構(gòu)造函數(shù),那是不是也允許在構(gòu)造函數(shù)中拋出異常呢?從Java語法上來說,完全可以在構(gòu)造函數(shù)中拋出異常,三類異常都可以,但是從系統(tǒng)設(shè)計(jì)和開發(fā)的角度來分析,則盡量不要在構(gòu)造函數(shù)中拋出異常,我們以三種不同類型的異常來說明之。
(1)、構(gòu)造函數(shù)中拋出錯誤是程序員無法處理的
在構(gòu)造函數(shù)執(zhí)行時(shí),若發(fā)生了VirtualMachineError虛擬機(jī)錯誤,那就沒招了,只能拋出,程序員不能預(yù)知此類錯誤的發(fā)生,也就不能捕捉處理。
(2)、構(gòu)造函數(shù)不應(yīng)該拋出非受檢異常
我們來看這樣一個(gè)例子,代碼如下:
class Person {
public Person(int _age) {
// 不滿18歲的用戶對象不能建立
if (_age < 18) {
throw new RuntimeException("年齡必須大于18歲.");
}
}
public void doSomething() {
System.out.println("doSomething......");
}
}
這段代碼的意圖很明顯,年齡不滿18歲的用戶不會生成一個(gè)Person實(shí)例對象,沒有對象,類行為doSomething方法就不可執(zhí)行,想法很好,但這會導(dǎo)致不可預(yù)測的結(jié)果,比如我們這樣引用Person類:
public static void main(String[] args) {
Person p = new Person(17);
p.doSomething();
/*其它的業(yè)務(wù)邏輯*/
}
很顯然,p對象不能建立,因?yàn)槭且粋€(gè)RunTimeException異常,開發(fā)人員可以捕捉也可以不捕捉,代碼看上去邏輯很正確,沒有任何瑕疵,但是事實(shí)上,這段程序會拋出異常,無法執(zhí)行。這段代碼給了我們兩個(gè)警示:
- 1.加重了上層代碼編寫者的負(fù)擔(dān):捕捉這個(gè)RuntimeException異常吧,那誰來告訴我有這個(gè)異常呢?只有通過文檔約束了,一旦Person類的構(gòu)造函數(shù)經(jīng)過重構(gòu)后再拋出其它非受檢異常,那main方法不用修改也是可以測試通過的,但是這里就可能會產(chǎn)生隱藏的缺陷,而寫還是很難重現(xiàn)的缺陷。不捕捉這個(gè)RuntimeException異常,這個(gè)是我們通常的想法,既然已經(jīng)寫成了非受檢異常,main方法的編碼者完全可以不處理這個(gè)異常嘛,大不了不執(zhí)行Person的方法!這是非常危險(xiǎn)的,一旦產(chǎn)生異常,整個(gè)線程都不再繼續(xù)執(zhí)行,或者鏈接沒有關(guān)閉,或者數(shù)據(jù)沒有寫入數(shù)據(jù)庫,或者產(chǎn)生內(nèi)存異常,這些都是會對整個(gè)系統(tǒng)產(chǎn)生影響。
- 2.后續(xù)代碼不會執(zhí)行:main方法的實(shí)現(xiàn)者原本是想把p對象的建立作為其代碼邏輯的一部分,執(zhí)行完doSomething方法后還需要完成其它邏輯,但是因?yàn)闆]有對非受檢異常進(jìn)行捕捉,異常最終會拋出到JVM中,這會導(dǎo)致整個(gè)線程執(zhí)行結(jié)束后,后面所有的代碼都不會繼續(xù)執(zhí)行了,這就對業(yè)務(wù)邏輯產(chǎn)生了致命的影響。
(3)、構(gòu)造函數(shù)盡可能不要拋出受檢異常
我們來看下面的例子,代碼如下:
//父類
class Base {
// 父類拋出IOException
public Base() throws IOException {
throw new IOException();
}
}
//子類
class Sub extends Base {
// 子類拋出Exception異常
public Sub() throws Exception {
}
}
就這么一段簡單的代碼,展示了在構(gòu)造函數(shù)中拋出受檢異常的三個(gè)不利方面:
- 1.導(dǎo)致子類膨脹:在我們的例子中子類的無參構(gòu)造函數(shù)不能省略,原因是父類的無參構(gòu)造函數(shù)拋出了IOException異常,子類的無參構(gòu)造函數(shù)默認(rèn)調(diào)用的是父類的構(gòu)造函數(shù),所以子類無參構(gòu)造函數(shù)也必須拋出IOException或其父類。
- 2.違背了里氏替換原則:"里氏替換原則" 是說父類能出現(xiàn)的地方子類就可以出現(xiàn),而且將父類替換為子類也不會產(chǎn)生任何異常。那我們回頭看看Sub類是否可以替換Base類,比如我們的上層代碼是這樣寫的:
public static void main(String[] args) {
try {
Base base = new Base();
} catch (Exception e) {
e.printStackTrace();
}
}
然后,我們期望把new Base()替換成new Sub(),而且代碼能夠正常編譯和運(yùn)行。非??上?,編譯不通過,原因是Sub的構(gòu)造函數(shù)拋出了Exception異常,它比父類的構(gòu)造函數(shù)拋出更多的異常范圍要寬,必須增加新的catch塊才能解決。
可能大家要問了,為什么Java的構(gòu)造函數(shù)允許子類的構(gòu)造函數(shù)拋出更廣泛的異常類呢?這正好與類方法的異常機(jī)制相反,類方法的異常是這樣要求的:
// 父類
class Base {
// 父類方法拋出Exception
public void testMethod() throws Exception {
}
}
// 子類
class Sub extends Base {
// 父類方法拋出Exception
@Override
public void testMethod() throws IOException {
}
}
子類的方法可以拋出多個(gè)異常,但都必須是覆寫方法的子類型,對我們的例子來說,Sub類的testMethod方法拋出的異常必須是Exception的子類或Exception類,這是Java覆寫的要求。構(gòu)造函數(shù)之所以于此相反,是因?yàn)闃?gòu)造函數(shù)沒有覆寫的概念,只是構(gòu)造函數(shù)間的引用調(diào)用而已,所以在構(gòu)造函數(shù)中拋出受檢異常會違背里氏替換原則原則,使我們的程序缺乏靈活性。
- 3.子類構(gòu)造函數(shù)擴(kuò)展受限:子類存在的原因就是期望實(shí)現(xiàn)擴(kuò)展父類的邏輯,但父類構(gòu)造函數(shù)拋出異常卻會讓子類構(gòu)造函數(shù)的靈活性大大降低,例如我們期望這樣的構(gòu)造函數(shù)。
// 父類
class Base {
public Base() throws IOException{
}
}
// 子類
class Sub extends Base {
public Sub() throws Exception{
try{
super();
}catch(IOException e){
//異常處理后再拋出
throw e;
}finally{
//收尾處理
}
}
}
很不幸,這段代碼編譯不通過,原因是構(gòu)造函數(shù)Sub沒有把super()放在第一句話中,想把父類的異常重新包裝再拋出是不可行的(當(dāng)然,這里有很多種 “曲線” 的實(shí)現(xiàn)手段,比如重新定義一個(gè)方法,然后父子類的構(gòu)造函數(shù)都調(diào)用該方法,那么子類構(gòu)造函數(shù)就可以自由處理異常了),這是Java語法機(jī)制。
將以上三種異常類型匯總起來,對于構(gòu)造函數(shù),錯誤只能拋出,這是程序人員無能為力的事情;非受檢異常不要拋出,拋出了 " 對己對人 " 都是有害的;受檢異常盡量不拋出,能用曲線的方式實(shí)現(xiàn)就用曲線方式實(shí)現(xiàn),總之一句話:在構(gòu)造函數(shù)中盡可能不出現(xiàn)異常。
注意 :在構(gòu)造函數(shù)中不要拋出異常,盡量曲線實(shí)現(xiàn)。
建議117:多使用異常,把性能問題放一邊
我們知道異常是主邏輯的例外邏輯,舉個(gè)簡單的例子來說,比如我在馬路上走(這是主邏輯),突然開過一輛車,我要避讓(這是受檢異常,必須處理),繼續(xù)走著,突然一架飛機(jī)從我頭頂飛過(非受檢異常),我們可以選在繼續(xù)行走(不捕捉),也可以選擇指責(zé)其噪音污染(捕捉,主邏輯的補(bǔ)充處理),再繼續(xù)走著,突然一顆流星砸下來,這沒有選擇,屬于錯誤,不能做任何處理。這樣具備完整例外場景的邏輯就具備了OO的味道,任何一個(gè)事務(wù)的處理都可能產(chǎn)生非預(yù)期的效果,問題是需要以何種手段來處理,如果不使用異常就需要依靠返回值的不同來進(jìn)行處理了,這嚴(yán)重失去了面向?qū)ο蟮娘L(fēng)格。
我們在編寫用例文檔(User case Specification)時(shí),其中有一項(xiàng)叫做 " 例外事件 ",是用來描述主場景外的例外場景的,例如用戶登錄的用例,就會在" 例外事件 "中說明" 連續(xù)3此登錄失敗即鎖定用戶賬號 ",這就是登錄事件的一個(gè)異常處理,具體到我們的程序中就是:
public void login(){
try{
//正常登陸
}catch(InvalidLoginException lie){
// 用戶名無效
}catch(InvalidPasswordException pe){
//密碼錯誤的異常
}catch(TooMuchLoginException){
//多次登陸失敗的異常
}
}
如此設(shè)計(jì)則可以讓我們的login方法更符合實(shí)際的處理邏輯,同時(shí)使主邏輯(正常登錄,try代碼塊)更加清晰。當(dāng)然了,使用異常還有很多優(yōu)點(diǎn),可以讓正常代碼和異常代碼分離、能快速查找問題(棧信息快照)等,但是異常有一個(gè)缺點(diǎn):性能比較慢。
Java的異常機(jī)制確實(shí)比較慢,這個(gè)"比較慢"是相對于諸如String、Integer等對象來說的,單單從對象的創(chuàng)建上來說,new一個(gè)IOException會比String慢5倍,這從異常的處理機(jī)制上也可以解釋:因?yàn)樗獔?zhí)行fillInStackTrace方法,要記錄當(dāng)前棧的快照,而String類則是直接申請一個(gè)內(nèi)存創(chuàng)建對象,異常類慢一籌也就在所難免了。
而且,異常類是不能緩存的,期望先建立大量的異常對象以提高異常性能也是不現(xiàn)實(shí)的。
難道異常的性能問題就沒有任何可以提高的辦法了?確實(shí)沒有,但是我們不能因?yàn)樾阅軉栴}而放棄使用異常,而且經(jīng)過測試,在JDK1.6下,一個(gè)異常對象的創(chuàng)建時(shí)間只需1.4毫秒左右(注意是毫秒,通常一個(gè)交易是在100毫秒左右),難道我們的系統(tǒng)連如此微小的性能消耗都不予許嗎?
注意:性能問題不是拒絕異常的借口。
建議121:線程優(yōu)先級只使用三個(gè)等級
線程的優(yōu)先級(Priority)決定了線程獲得CPU運(yùn)行的機(jī)會,優(yōu)先級越高獲得的運(yùn)行機(jī)會越大,優(yōu)先級越低獲得的機(jī)會越小。Java的線程有10個(gè)級別(準(zhǔn)確的說是11個(gè)級別,級別為0的線程是JVM的,應(yīng)用程序不能設(shè)置該級別),那是不是說級別是10的線程肯定比級別是9的線程先運(yùn)行呢?我們來看如下一個(gè)多線程類:
class TestThread implements Runnable {
public void start(int _priority) {
Thread t = new Thread(this);
// 設(shè)置優(yōu)先級別
t.setPriority(_priority);
t.start();
}
@Override
public void run() {
// 消耗CPU的計(jì)算
for (int i = 0; i < 100000; i++) {
Math.hypot(924526789, Math.cos(i));
}
// 輸出線程優(yōu)先級
System.out.println("Priority:" + Thread.currentThread().getPriority());
}
}
該多線程實(shí)現(xiàn)了Runnable接口,實(shí)現(xiàn)了run方法,注意在run方法中有一個(gè)比較占用CPU的計(jì)算,該計(jì)算毫無意義,
public static void main(String[] args) {
//啟動20個(gè)不同優(yōu)先級的線程
for (int i = 0; i < 20; i++) {
new TestThread().start(i % 10 + 1);
}
}
這里創(chuàng)建了20個(gè)線程,每個(gè)線程在運(yùn)行時(shí)都耗盡了CPU的資源,因?yàn)閮?yōu)先級不同,線程調(diào)度應(yīng)該是先處理優(yōu)先級高的,然后處理優(yōu)先級低的,也就是先執(zhí)行2個(gè)優(yōu)先級為10的線程,然后執(zhí)行2個(gè)優(yōu)先級為9的線程,2個(gè)優(yōu)先級為8的線程......但是結(jié)果卻并不是這樣的。
Priority:5
Priority:7
Priority:10
Priority:6
Priority:9
Priority:6
Priority:5
Priority:7
Priority:10
Priority:3
Priority:4
Priority:8
Priority:8
Priority:9
Priority:4
Priority:1
Priority:3
Priority:1
Priority:2
Priority:2
println方法雖然有輸出損耗,可能會影響到輸出結(jié)果,但是不管運(yùn)行多少次,你都會發(fā)現(xiàn)兩個(gè)不爭的事實(shí):
(1)、并不是嚴(yán)格按照線程優(yōu)先級來執(zhí)行的
比如線程優(yōu)先級為5的線程比優(yōu)先級為7的線程先執(zhí)行,優(yōu)先級為1的線程比優(yōu)先級為2的線程先執(zhí)行,很少出現(xiàn)優(yōu)先級為2的線程比優(yōu)先級為10的線程先執(zhí)行(注意,這里是" 很少 ",是說確實(shí)有可能出現(xiàn),只是幾率低,因?yàn)閮?yōu)先級只是表示線程獲得CPU運(yùn)行的機(jī)會,并不代表強(qiáng)制的排序號)。
(2)、優(yōu)先級差別越大,運(yùn)行機(jī)會差別越明顯
比如優(yōu)先級為10的線程通常會比優(yōu)先級為2的線程先執(zhí)行,但是優(yōu)先級為6的線程和優(yōu)先級為5的線程差別就不太明顯了,執(zhí)行多次,你會發(fā)現(xiàn)有不同的順序。
這兩個(gè)現(xiàn)象是線程優(yōu)先級的一個(gè)重要表現(xiàn),之所以會出現(xiàn)這種情況,是因?yàn)榫€程運(yùn)行是需要獲得CPU資源的,那誰能決定哪個(gè)線程先獲得哪個(gè)線程后獲得呢?這是依照操作系統(tǒng)設(shè)置的線程優(yōu)先級來分配的,也就是說,每個(gè)線程要運(yùn)行,需要操作系統(tǒng)分配優(yōu)先級和CPU資源,對于JAVA來說,JVM調(diào)用操作系統(tǒng)的接口設(shè)置優(yōu)先級,比如windows操作系統(tǒng)優(yōu)先級都相同嗎?
事實(shí)上,不同的操作系統(tǒng)線程優(yōu)先級是不同的,Windows有7個(gè)優(yōu)先級,Linux有140個(gè)優(yōu)先級,F(xiàn)reebsd則由255個(gè)(此處指的優(yōu)先級個(gè)數(shù),不同操作系統(tǒng)有不同的分類,如中斷級線程,操作系統(tǒng)級等,各個(gè)操作系統(tǒng)具體用戶可用的線程數(shù)量也不相同)。Java是跨平臺的系統(tǒng),需要把這10個(gè)優(yōu)先級映射成不同的操作系統(tǒng)的優(yōu)先級,于是界定了Java的優(yōu)先級只是代表搶占CPU的機(jī)會大小,優(yōu)先級越高,搶占CPU的機(jī)會越大,被優(yōu)先執(zhí)行的可能性越高,優(yōu)先級相差不大,則搶占CPU的機(jī)會差別也不大,這就是導(dǎo)致了優(yōu)先級為9的線程可能比優(yōu)先級為10的線程先運(yùn)行。
Java的締造者們也覺察到了線程優(yōu)先問題,于是Thread類中設(shè)置了三個(gè)優(yōu)先級,此意就是告訴開發(fā)者,建議使用優(yōu)先級常量,而不是1到10的隨機(jī)數(shù)字。常量代碼如下:
public class Thread implements Runnable {
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
}
在編碼時(shí)直接使用這些優(yōu)先級常量,可以說在大部分情況下MAX_PRIORITY的線程回比MIN_PRIORITY的線程優(yōu)先運(yùn)行,但是不能認(rèn)為是必然會先運(yùn)行,不能把這個(gè)優(yōu)先級做為核心業(yè)務(wù)的必然條件,Java無法保證優(yōu)先級高肯定會先執(zhí)行,只能保證高優(yōu)先級有更多的執(zhí)行機(jī)會。因此,建議在開發(fā)時(shí)只使用此三類優(yōu)先級,沒有必要使用其他7個(gè)數(shù)字,這樣也可以保證在不同的操作系統(tǒng)上優(yōu)先級的表現(xiàn)基本相同。
大家也許會問,如果優(yōu)先級相同呢?這很好辦,也是由操作系統(tǒng)決定的。基本上是按照FIFO原則(先入先出,F(xiàn)irst Input First Output),但也是不能完全保證。
歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學(xué)習(xí)之路以及各種Java學(xué)習(xí)資料