鄭重宣告
本文內容來源於《Java解惑》一書,不是本人所獨創,請尊重原創作者的勞動成果和著作的知識版權。本文摘錄原著內容,並做部分學習標記,僅作為自己的學習筆記
數值表示式篇
1. 奇偶判斷
不要使用 i % 2 == 1 來判斷是否是奇數,因為i為負奇數時不成立,請使用 i % 2 != 0 來判斷是否是奇數,或使用高效式 (i & 1) != 0來判斷。
2. 小數精確計算
System.out.println(2.00 -1.10); //0.8999999999999999
上面的計算出的結果不是 0.9,而是一連串的小數。問題在於1.1這個數字不能被精確表示為一個double,因此它被表示為最接近它的double值,該程式從2中減去的就是這個值,但這個計算的結果並不是最接近0.9的double值。
一般地說,問題在於並不是所有的小數都可以用二進位制浮點數精確表示。
二進位制浮點對於貨幣計算是非常不適合的,因為它不可能將1.0表示成10的其他任何負次冪。
解決問題的第一種方式是使用貨幣的最小單位(分)來表示:
System.out.println(200-110); //90
第二種方式是使用BigDecimal,但一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)來構造(也不能將float或double型轉換成String再來使用BigDecimal(String)來構造,因為在將float或double轉換成String時精度已丟失)。例如new BigDecimal(0.1),它將返回一個BigDecimal,也即
0.1000000000000000055511151231257827021181583404541015625,正確使用BigDecimal,程式就可以列印出我們所期望的結果0.9:
System.out.println(new BigDecimal("2.0").subtract(new BigDecimal("1.10")));// 0.9
另外,如果要比較兩個浮點數的大小,要使用BigDecimal的compareTo方法。
如果你還想更深入瞭解下,請參考《Java中的浮點數剖析》
3. int整數相乘溢位
我們計算一天中的微秒數:
long microsPerDay = 24 * 60 * 60 * 1000 * 1000; // 正確結果應為:86400000000
System.out.println(microsPerDay); // 實際上為:500654080
問題在於計算過程中溢位了。這個計算式完全是以int運算來執行的,並且只有在運算完成之後,其結果才被提升為long,而此時已經太遲:計算已經溢位。
解決方法使計算表示式的第一個因子明確為long型,這樣可以強制表示式中所有的後續計算都用long運算來完成,這樣結果就不會溢位:
long microsPerDay = 24L * 60 * 60 * 1000 * 1000;
4. 負的十六進位制與八進位制字面常量
“數字字面常量”的型別都是int型,而不管他們是幾進位制,所以“2147483648”、“0x180000000(十六進位制,共33位,所以超過了整數的取值範圍)”字面常量是錯誤的,編譯時會報超過int的取值範圍了,所以要確定以long來表示“2147483648L”、“0x180000000L”。
十進位制字面常量只有一個特性,即所有的十進位制字面常量都是正數,如果想寫一個負的十進位制,則需要在正的十進位制字面常量前加上“-”即可。
十六進位制或八進位制字面常量可就不一定是正數或負數,是正還是負,則要根據當前情況看:如果十六進位制和八進位制字面常量的最高位被設定成了1,那麼它們就是負數:
1. System.out.println(0x80);//128
2. //0x81看作是int型,最高位(第32位)為0,所以是正數
3. System.out.println(0x81);//129
4. System.out.println(0x8001);//32769
5. System.out.println(0x70000001);//1879048193
6. //字面量0x80000001為int型,最高位(第32位)為1,所以是負數
7. System.out.println(0x80000001);//-2147483647
8. //字面量0x80000001L強制轉為long型,最高位(第64位)為0,所以是正數
9. System.out.println(0x80000001L);//2147483649
10. //最小int型
11. System.out.println(0x80000000);//-2147483648
12. //只要超過32位,就需要在字面常量後加L強轉long,否則編譯時出錯
13. System.out.println(0x8000000000000000L);//-9223372036854775808
從上面可以看出,十六進位制的字面常量表示的是int型,如果超過32位,則需要在後面加“L”,否則編譯過不過。如果為32,則為負int正數,超過32位,則為long型,但需明確指定為long。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));// cafebabe
結果為什麼不是0x1cafebabe?該程式執行的加法是一個混合型別的計算:左運算元是long型,而右運算元是int型別。為了執行該計算,Java將int型別的數值用拓寬原生型別轉換提升為long型別,然後對兩個long型別數值相加。因為int是有符號的整數型別,所以這個轉換執行的是符號擴充套件。
這個加法的右運算元0xcafebabe為32位,將被提升為long型別的數值0xffffffffcafebabeL,之後這個數值加上了左操作0x100000000L。當視為int型別時,經過符號擴充套件之後的右運算元的高32位是-1,而左運算元的第32位是1,兩個數值相加得到了0:
0x 0xffffffffcafebabeL
+0x 0000000100000000L
-----------------------------
0x 00000000cafebabeL
如果要得到正確的結果0x1cafebabe,則需在第二個運算元組後加上“L”明確看作是正的long型即可,此時相加時擴充符號位就為0:
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));// 1cafebabe
5. 窄數字型別提升至寬型別時使用符號位擴充套件還是零擴充套件
System.out.println((int)(char)(byte)-1);// 65535
結果為什麼是65535而不是-1?
窄的整型轉換成較寬的整型時符號擴充套件規則:如果最初的數值型別是有符號的,那麼就執行符號擴充套件(即如果符號位為1,則擴充套件為1,如果為零,則擴充套件為0);如果它是char,那麼不管它將要被提升成什麼型別,都執行零擴充套件。
瞭解上面的規則後,我們再來看看迷題:因為byte是有符號的型別,所以在將byte數值-1(二進位制為:11111111)提升到char時,會發生符號位擴充套件,又符號位為1,所以就補8個1,最後為16個1;然後從char到int的提升時,由於是char型提升到其他型別,所以採用零擴充套件而不是符號擴充套件,結果int數值就成了65535。
如果將一個char數值c轉型為一個寬度更寬的型別時,只是以零來擴充套件,但如果清晰表達以零擴充套件的意圖,則可以考慮使用一個位掩碼:
int i = c & 0xffff;//實質上等同於:int i = c ;
如果將一個char數值c轉型為一個寬度更寬的整型,並且希望有符號擴充套件,那麼就先將char轉型為一個short,它與char上個具有同樣的寬度,但是它是有符號的:
int i = (short)c;
如果將一個byte數值b轉型為一個char,並且不希望有符號擴充套件,那麼必須使用一個位掩碼來限制它:
char c = (char)(b & 0xff);// char c = (char) b;為有符號擴充套件
6. ((byte)0x90 == 0x90)?
答案是不等的,儘管外表看起來是成立的,但是它卻等於false。為了比較byte數值(byte)0x90和int數值0x90,Java通過拓寬原生型別將byte提升為int,然後比較這兩個int數值。因為byte是一個有符號型別,所以這個轉換執行的是符號擴充套件,將負的byte數值提升為了在數字上相等的int值(10010000à111111111111111111111111 10010000)。在本例中,該轉換將(byte)0x90提升為int數值-112,它不等於int數值的0x90,即+144。
解決辦法:使用一個遮蔽碼來消除符號擴充套件的影響,從而將byte轉型為int
((byte)0x90 & 0xff)== 0x90
7. 三元表示式(?:)
char x = 'X';
int i = 0;
System.out.println(true ? x : 0);// X
System.out.println(false ? i : x);// 88
條件表示式結果型別的規則:
(1) 如果第二個和第三個運算元具有相同的型別,那麼它就是條件表示式的型別。
(2) 如果一個操作的型別是T,T表示byte、short或char,而另一個運算元是一個int型別的“字面常量”,並且它的值可以用型別T表示,那條件表示式的型別就是T。
(3) 否則,將對運算元型別進行提升,而條件表示式的型別就是第二個和第三個操作被提升之後的型別。
現來使用以上規則解上面的迷題,第一個表示式符合第二條規則:一個運算元的型別是char,另一個的型別是字面常量為0的int型,但0可以表示成char,所以最終返回型別以char型別為準;第二個表示式符合第三條規則:因為i為int型變數,而x又為char型變數,所以會先將x提升至int型,所以最後的結果型別為int型,但如果將i定義成final時,
則返回結果型別為char,則此時符合第二條規則,因為final型別的變數在編譯時就使用“字面常量0”來替換三元表示式了:
final int i = 0;
System.out.println(false ? i : x);// X
在JDK1.4版本或之前,條件操作符 ?: 中,當第二個和延續三個運算元是引用型別時,條件操作符要求它們其中一個必須是另一個的子型別,那怕它們有同一個父類也不行:
public class T {
public static void main(String[] args) {
System.out.println(f());
}
public static T f() {
// !!1.4不能編譯,但1.5可以
// !!return true?new T1():new T2();
return true ? (T) new T1() : new T2();// T1
}
}
class T1 extends T {
public String toString() {
return "T1";
}
}
class T2 extends T {
public String toString() {
return "T2";
}
}
在5.0或以上版本中,條件操作符在延續二個和第三個運算元是引用型別時總是合法的。其結果型別是這兩種型別的最小公共超類。公共超類總是存在的,因為Object是每一個物件型別的超型別,上面的最小公共超類是T,所以能編譯。
8. +=複合賦值問題
x+=i與x=x+i等效嗎,許多程式設計師都會認為第一個表示式x+=i只是第二個表示式x=x+i的簡寫方式,但這並不準確。
Java語言規範中提到:複合賦值 E1 op= E2等價於簡單賦值 E1 = (T)((E1) op (E2)),其中T是E1的型別。
複合賦值表示式自動地將所執行計算的結果轉型為其左側變數的型別。如果結果的型別與該變數的型別相同,那麼這個轉型不會造成任何影響,然而,如果結果的型別比該變數的型別要寬,那麼複合賦值操作符將悄悄地執行一個窄化原生型別轉換,這樣就會導致結果不正確:
short x=0;
int i = 123456;
x +=i;
System.out.println(x);//-7616
使用簡單的賦值方式就不會有這樣的問題了,因為寬型別不能自動轉換成窄的型別,編譯器會報錯,這時我們就會注意到錯誤:x = x + i;//編譯通不過
請不要將複合賦值操作符作用於byte、short或char型別的變數;在將複合賦值操作符作用於int型別的變數時,要確保表示式右側不是long、float或double型別;在將複合賦值操作符作用於float型別的變數時,要確保表示式右側不是double型別。其實一句:不要將讓左側的型別窄於右側的數字型別。
總之,不要在short、byte或char型別的變數之上使用複合賦值操作符,因為這一過程會伴隨著計算前型別的提升與計算後結果的截斷,導致最後的計算結果不正確。
9. i =++i;與i=i++;的區別
int i = 0;
i = i++;
System.out.println(i);
上面的程式會輸出什麼?大部分會說是 1,是也,非也。執行時正確結果為0。
i=++i;相當於以下二個語句(編譯時出現警告,與i=i;警告相同):
i=i+1;
i=i;
i = i++;相當於以下三個語句:
int tmp = i;
i = i + 1;
i = tmp;
下面看看下面程式片段:
int i = 0, j = 0, y = 0;
i++;//相當於:i=i+1;
System.out.println("i=" + i);// i=1
++i;//相當於:i=i+1;
System.out.println("i=" + i);// i=2
i = i++;//相當於:int tmp=i;i=i+1;i=tmp;
System.out.println("i=" + i);// i=2
i = ++i;//編譯時出現警告,與i=i;警告相同。相當於:i=i+1;i=i;
System.out.println("i=" + i);// i=3
j = i++;//相當於:int tmp=i;i=i+1;j=tmp;
System.out.println("j=" + j);// j=3
System.out.println("i=" + i);// i=4
y = ++i;//相當於:i=i+1;y=i;
System.out.println("y=" + y);// y=5
System.out.println("i=" + i);// i=5
10. Integer.MAX_VALUE + 1=?
1. System.out.println(Integer.MAX_VALUE + 1);
上面的程式輸出多少?2147483647+1=2147483648?答案為-2147483648。
檢視原始碼Integer.MAX_VALUE 為MAX_VALUE = 0x7fffffff;所以加1後為0x80000000,又0x80000000為整型字面常量,滿了32位,且最位為1,所以字面上等於 -0,但又由於 -0就是等於0,所以-0這個編碼就規定為最小的負數,32位的最小負數就是-2147483648。
11. -1<<32=?、-1<<65=?
如果左運算元是int(如果是byte、short、char型時會提升至int型再進行位操作)型,移位操作符只使用其右運算元的低5位作為移位長度(也就是將右運算元除以32取餘);如果左運算元是long型,移位操作符只使用其右運算元的低6位作為移位長度(也就是將右運算元除以64取餘);
再看看下面程式片段就會知道結果:
System.out.println(-1 << 31);// -2147483648 向左移31%32=31位
System.out.println(-1 << 32);// -1 向左移32%32=0位
System.out.println(-1 << 33);// -2 向左移33%32=1位
System.out.println(-1 << 1);// -2 向左移1%32=1位
System.out.println(-1L << 63);// -9223372036854775808 向左移63%64=63位
System.out.println(-1L << 64);// -1 向左移64%64=0位
System.out.println(-1L << 65);// -2 向左移65%64=1位
System.out.println(-1L << 1);// -2 向左移1%64=1位
byte b = -1;// byte型在位操作前型別提升至int
System.out.println(b << 31);// -2147483648 向左移31%32=31位
System.out.println(b << 63);// -2147483648 向左移63%32=31位
short s = -1;// short型在位操作前型別提升至int
System.out.println(s << 31);// -2147483648 向左移31%32=31位
System.out.println(s << 63);// -2147483648 向左移63%32=31位
char c = 1;// char型在位操作前型別提升至int
System.out.println(c << 31);// -2147483648 向左移31%32=31位
System.out.println(c << 63);// -2147483648 向左移63%32=31位
12. 一個數永遠不會等於它自己加1嗎?i==i+1
一個數永遠不會等於它自己加1,對嗎?如果數字是整型,則對;如果這個數字是無窮大或都是浮點型足夠大(如1.0e40),等式就可能成立了。
Java強制要求使用IEEE 754浮點數算術運算,它可以讓你用一個double或float來表示無窮大。
浮點型分為double型、float型。
無窮分為正無窮與負無窮。
無窮大加1還是無窮大。
一個浮點數值越大,它和其後繼數值之間的間隔就越大。
對一個足夠大的浮點數加1不會改變它的值,因為1不足以“填補它與其後者之間的空隙”。
浮點數操作返回的是最接近其精確數學結果的浮點數值。
一旦毗鄰的浮點數值之間的距離大於2,那麼對其中的一個浮點數值加1將不會產生任何效果,因為其結果沒有達到兩個數值之間的一半。對於float型別,加1不會產生任何效果的最小數是2^25,即33554432;而對於double型別,最小數是2^54,大約是1.8*10^16。
33554432F轉二進位制過程:
33554432的二進位制為:10000000000000000000000000,將該二進位制化成規範的小數二進位制,即小數從右向左移25位
1.0000000000000000000000000,化成浮點數二進位制0,25+127, 00000000000000000000000 00(丟棄最後兩位),即0, 10011000, 00000000000000000000000,最後的結果為1.00000000000000000000000*2^25
毗鄰的浮點數值之間的距離被稱為一個ulp,它是最小單位(unit in the last place)的首字母縮寫。在5.0版本中,引入了Math.ulp方法來計算float或double數值的ulp。
二進位制浮點算術只是對實際算術的一種近似。
// 注,整型數不能被 0 除,即(int)XX/0執行時拋異常
double i = 1.0 / 0.0;// 正無窮大
double j = -1.0 / 0.0;// 負無窮大
// Double.POSITIVE_INFINITY定義為:POSITIVE_INFINITY = 1.0 / 0.0;
System.out.println(i + " " + (i == Double.POSITIVE_INFINITY));//Infinity true
// Double.NEGATIVE_INFINITY定義為:NEGATIVE_INFINITY = -1.0 / 0.0;
System.out.println(j + " " + (j == Double.NEGATIVE_INFINITY));//-Infinity true
System.out.println(i == (i + 1));// true
System.out.println(0.1f == 0.1);// false
float f = 33554432;
System.out.println(f + " " + (f==(f+1)));//3.3554432E7 true
13. 自己不等於自己嗎?i!=i
NaN(Not a Number)不等於任何數,包括它自身在內。
double i = 0.0/0.0;可表示NaN。
float和double型別都有一個特殊的NaN值,Double.NaN、Float.NaN表示NaN。
如果一個表示式中產生了NaN,則結果為NaN。
System.out.println(0.0 / 0.0);// NaN
System.out.println(Double.NaN + " " + (Double.NaN == (0.0 / 0.0)));//NaN false
14. 自動拆箱
// 為了相容以前版本,1.5不會自動拆箱
System.out.println(new Integer(0) == new Integer(0));// false
// 1.4編譯非法,1.5會自動拆箱
System.out.println(new Integer(0) == 0);// true
15. 為什麼-0x00000000==0x00000000、-0x80000000== 0x80000000
為了取一個整數型別的負值,要對其每一位取反(如果是對某個十六進位制形式整數求負,如:-0x00000000則直接對這個十六進位制數進行各位取反操作——但不包括前面的負號;如果是對某個十進位制求負,如-0,則需先求其絕對值的十六進位制的原碼後,再各位取反),然後再加1。
注:如果是對某個十進位制數求負,如-1(0xffffffff),實質上按照平時求一個負數補碼的方式來處理也是一樣的,求某個負數的補碼規則為:先求這個數絕對值的原碼,然後從該二進位制的右邊開始向左找第一個為1的位置,最後將這個1前的各位取反(包括最高位符號位,即最高位0取反後為1),其他位不變,最終所得的二進位制就為這個負數的補碼,也就是最終在記憶體中負數所表示的形式。不過在找這個第一個為1時可能找不到或在最高位,比如-0,其絕對值為0(0x00000000);也有可能最高位為1,比如-2147483648,其絕對值為2147483648(0x80000000),如果遇到絕對值的原碼為0x00000000或0x80000000的情況下則不變,即為絕對值的原碼本身。
-0x00000000的運算過程:對0x00000000先取反得到0xffffffff,再加1,-0x00000000的最後結果就為 0xffffffff+1,其最後的結果還是0x00000000,所以-0x00000000 == 0x00000000。前面是對0x00000000求負的過程,如果是對0求負呢?先求0的十六進位制形式0x00000000,再按前面的過程來即可。或者根據前面規則對0x00000000求負不變,即最後結果還是0x00000000。
-0x80000000的運算過程:對0x80000000先取反得到0x7fffffff,再加1,-0x80000000的最後結果就為 0x7fffffff+1,其最後的結果還是0x80000000,即-0x80000000 == 0x80000000。前面是對0x80000000求負的過程,如果是對2147483648求負呢?先求2147483648的十六進位制形式0x80000000,再按前面的過程來即可。或者根據前面規則對0x80000000求負不變,即最後結果還是0x80000000。
-0x00000001的運算過程,實質上就是求-1的補碼過程,即對其絕對值的十六進位制0x00000001求補碼,即為0xffffffff,即-1的補碼為0xffffffff。
System.out.println(Integer.MIN_VALUE == -Integer.MIN_VALUE);// true
/*
* 0x80000000取反得0x7fffffff,再加1得0x80000000,因為負數是
* 以補碼形式儲存於記憶體中的,所以推匯出結果原碼為:0x80000000,
* 即為-0,又因為-0是等於0的,所以不需要-0這個編碼位,那就多了
* 一個0x80000000編碼位了,所以最後就規定0x80000000為最小負數
*/
System.out.println(-0x80000000);// -2147483648
/*
* 0x7fffffff取反得0x80000000,再加1得0x80000001,因為負數是
* 以補碼形式儲存於記憶體中的,所以推匯出結果原碼為:0xffffffff,
* 第一位為符號位,所以最後的結果就為 -0x7fffffff = -2147483647
*/
System.out.println(-0x7fffffff);// -2147483647
另外,還發現有趣現象:最大整數加1後會等於最小整數:
// MAX_VALUE = 0x7fffffff; MIN_VALUE = 0x80000000;
System.out.println((Integer.MAX_VALUE + 1) == Integer.MIN_VALUE);// true
// MIN_VALUE = 0x8000000000000000L; MIN_VALUE = 0x8000000000000000L;
System.out.println((Long.MAX_VALUE + 1) == Long.MIN_VALUE);// true
當然,-Byte. MIN_VALUE==Byte.MIN_VALUE、-Short.MIN_VALUE== Short.MIN_VALUE、-Long.MIN_VALUE== Long.MIN_VALUE,也是成立的。
16. Math.abs結果一定為非負數嗎?
System.out.println(Math.abs(Integer.MIN_VALUE));// -2147483648
上面的程式不會輸出2147483648,而是-2147483648,為什麼?
其實我們看一下Math.abs原始碼就知道為什麼了,原始碼:(a < 0) ? -a : a;,結合上面那個迷題,我們就發現-Integer.MIN_VALUE= Integer.MIN_VALUE,所以上面的答案就是最小整數自己。
另外我們也可以從API文件看到對Math.abs()方法的解釋:如果引數等於 Integer.MIN_VALUE 的值(即能夠表示的最小負 int 值),則結果與該值相同且為負。
所以Math.abs不能保證一定會返回非負結果。
當然,Long.MIN_VALUE也是這樣的。
17. 不要使用基於減法的比較器
Comparator<Integer> c = new Comparator<Integer>() {
public int compare(Integer i1, Integer i2) {
return i1 - i2;// 升序
}
};
List<Integer> l = new ArrayList<Integer>();
l.add(new Integer(-2000000000));
l.add(new Integer(2000000000));
Collections.sort(l, c);
System.out.println(l);// [2000000000, -2000000000]
上面程式的比較器是升序,結果卻不是這樣,比較時出現了什麼問題?
先看看下面程式片斷:
int x = -2000000000;
int y = 2000000000;
/*
* -2000000000 即 -(01110111001101011001010000000000)
* 的補碼為: 10001000110010100110110000000000
*
* 計算過程使用豎式表示:
* 10001000110010100110110000000000
* 10001000110010100110110000000000
* --------------------------------
* 00010001100101001101100000000000
*
* 計算結果溢位,結果為294967296
*/
System.out.println(x - y);// 294967296
所以不要使用減法的比較器,除非能確保要比較的數值之間的距離永遠不會大於Intger. MAX_VALUE。
基於整型的比較器的實現一般使用如下的方式來比較:
public int compare(Integer i1, Integer i2) {
return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
}
18. int i=-2147483648與int i=-(2147483648)?
int i=-(2147483648);
編譯通不過!為什麼
int字面常量2147483638只能作為一元負操作符的運算元來使用。
類似的還有最大long:
long i=–(9223372036854775808L);
字串篇
19. char型別相加
System.out.println('a' + 'A');//162
上面的結果不是 aA ,而是 162。
當且僅當+操作符的運算元中至少有一個是String型別時,才會執行字串連線操作;否則,執行加法。如果要連線的
數值沒有一個是字串型別的,那麼你可以有幾種選擇:預置一個空字串("" + 'a' + 'A');將第一個數值用
String.valueOf()顯示地轉換成一個字串(String.valueOf('a') + 'A');使用一個字串緩衝區(sb.append
('a');sb.append('A'););或者如果使用的是JDK5.0,可以用printf(System.out.printf("%c%c",'a','A'));
20. 程式中的Unicode轉義字元
///u0022是雙引號的Unicode編碼表示
System.out.println("a/u0022.length() + /u0022b".length());// 2
Unicode編碼表示的字元是在編譯期間就轉換成了普通字元,它與普通轉義字元(如:/")是不一樣的,它們是在程式
被解析為各種符號之後才處理轉義字元序列。
21. 註釋中的Unicode轉義字元
如果程式中含有以下的註釋:// d:/a/b/util ,程式將不能編譯通過,原因是/u後面跟的不是四個十六進位制數字,但
編譯器在編譯時卻要把/u開頭的字元的字元看作是Unicode編碼表示的字元。
所以要注意:註釋中也是支援Unicode轉義字元的。
另外一個問題是不能在註釋行的中間含有 /u000A 表示換行的Unicode字元,因為這樣在編譯時讀到 /u000A 時,表示行結束,那麼後面的字元就會當作程式程式碼而不在是註釋了。
22. Windows與Linux上的行結束標示符
String line = (String)System.getProperties().get("line.separator");
for(int i =0; i < line.length();i++){
System.out.println((int)line.charAt(i));
}
在Windows上執行結果:
13
10
在Linux上執行的結果:
10
在Windows平臺上,行分隔符是由回車(/r)和緊其後的換行(/n)組成,但在Unix平臺上通常使用單獨的換行(/n)表示。
23. 輸出0-255之間的ISO8859-1符
byte bts[] = new byte[256];
for (int i = 0; i < 256; i++) {
bts[i] = (byte) i;
}
// String str = new String(bts,"ISO8859-1");//正確的做法
String str = new String(bts);//使用作業系統預設編碼方式編碼(XP GBK)
for (int i = 0, n = str.length(); i < n; i++) {
System.out.print((int) str.charAt(i) + " ");
}
上面不會輸出0-255之間的數字串,正確的方式要使用new String(bts," ISO8859-1") 方式來解碼。
ISO8859-1是唯一能夠讓該程式按順序列印從0到255的整數的缺少字符集,這也是唯一在字元和位元組之間一對一的對映字符集。
通過java獲取作業系統的預設編碼方式:
System.getProperty("file.encoding");//jdk1.4或之前版本
java.nio.charset.Charset.defaultCharset();//jdk1.5或之後版本
24. String的replace()與replaceAll()
System.out.println(".".replaceAll(".class", "//$"));
上面程式將 . 替換成 /$,但執行時報異常,主要原replaceAll的第二引數有兩個字元(/ $)是特殊字元,具有特殊意思(/用來轉移 / 與 $,$後面接數字表示反向引用)。另外,replaceAll的第一引數是正規表示式,所以要注意特殊字元,正確的作法有以下三種:
System.out.println(".class".replaceAll("//.", "//////$"));
System.out.println(".class".replaceAll("//Q.//E", "//////$"));
System.out.println(".class".replaceAll(Pattern.quote("."), Matcher.quoteReplacement("//$")));
API對/、/Q與/E的解釋:
/ 引用(轉義)下一個字元
/Q引用所有字元,直到 /E
/E結束從 /Q 開始的引用
JDK5.0新增了一些解決此問題的新方法:
java.util.regex.Pattern.quote(String s):使用/Q與/E將引數引起來,這些被引用的字串就是一般的字元,哪怕含有正則式特殊字元。
java.util.regex.Matcher.quoteReplacement(String s):將/與$轉換成能應用於replaceAll第二個引數的字串,即可作為替換內容。
String的replace(char oldChar, char newChar)方法卻不使用正則式,但它們只支援字元,而不是字串,使用起來受限制:
System.out.println(".".replace('.','//'));//能將 . 替換成 /
System.out.println(".".replace('.','$')); //能將 . 替換成 $
25. 一段程式的三個Bug
Random rnd = new Random();
StringBuffer word = null;
switch (rnd.nextInt(2)) {
case 1:
word = new StringBuffer('P');
case 2:
word = new StringBuffer('G');
default:
word = new StringBuffer('M');
}
word.append('a');
word.append('i');
word.append('n');
System.out.println(word);
上面的程式目的是等概率的列印 Pain、Gain、Main 三個單詞,但多次執行程式卻發現永遠只會列印 ain,這是為什麼?
第一個問題在於:rnd.nextInt(2)只會返回0、1 兩個數字,所以上面只會走case 1: 的分支語句,case 2: 按理是永遠不會走的。
第二個問題在於:如果case語句不以break結束時,則一直會往向執行,即直到執行到break的case語句止,所以上面的的語句每次都會執行default分支語句。
第三個問題在於:StringBuffer的建構函式有兩種可接受引數的,一個是StringBuffer(int capacity)、另一個是StringBuffer(String str),上面用的是StringBuffer(char)建構函式,實質上執行時將字元型轉換成了int型,這樣將字元當作StringBuffer的初始容量了,而不是字元本身。
以下是修改後的程式片段:
Random rnd = new Random();
StringBuffer word = null;
switch (rnd.nextInt(3)) {
case 1:
word = new StringBuffer("P");
break;
case 2:
word = new StringBuffer("G");
break;
default:
word = new StringBuffer("M");
break;// 可以不要
}
word.append('a');
word.append('i');
word.append('n');
System.out.println(word);
異常篇
26. finally與中斷
//該方法返回false
static boolean f() {
try {
return true;
} finally {
return false;
}
}
不要用return、break、continue或throw來退出finally語句塊,並且千萬不要允許受檢查的異常傳播到finally語句
塊之外。也就是說不要在finally塊內終止程式,而是執行完finally塊後,要將控制權移交給try塊,由try最終決定
怎樣結束方法的呼叫。
對於任何在finally語句塊中可能丟擲的受檢查異常都要進行處理,而不是任其傳播,下面流拷貝程式在關閉流時沒有
防止異常的傳播,這會有問題:
static void copy(String src, String dest) throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(src);
out = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally{
//這裡應該使用try-catch將每個close包裝起來
if(in != null){in.close();}
if(in != null){out.close();}
}
}
catch塊中的return語句是不會阻止finally塊執行的,那麼catch塊中的continue和break能否阻止?答案是不會的,
與return一樣,finally語句塊是在迴圈被跳過(continue)和中斷(break)之前被執行的:
int i = 0;
System.out.println("--continue--");
while (i++ <= 1) {
try {
System.out.println("i=" + i);
continue;
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
System.out.println("--break--");
while (i++ <= 3) {
try {
System.out.println("i=" + i);
break;
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
27. catch捕獲異常規則
捕獲RuntimeException、Exception或Throwable的catch語句是合法,不管try塊裡是否丟擲了這三個異常。但如果try
塊沒有丟擲或不可能丟擲檢測性異常,則catch不能捕獲這些異常,如IOException異常:
public class Test {
public static void main(String[] args) {
try{
//...
}catch (Exception e) {
}catch (Throwable e) {
}
/* !! 編譯出錯
try{
//...
}catch (IOException e) {
}
*/
}
}
28. 重寫時方法異常範圍
重寫或實現時不能擴大異常的範圍,如果是多繼承,則異常取所有父類方法異常的交集或不丟擲異常:
interface I1 {
void f() throws Exception;
}
interface I2 {
void f() throws IOException;
}
interface I3 extends I1, I2 {}
class Imp implements I3 {
// 不能編譯通過,多繼承時只能取父類方法異常交集,這樣就不會擴大異常範圍
// !! void f () throws Exception;
// void f();// 能編譯通過
// 能編譯通過,Exception與IOException的交集為IOException
public void f() throws IOException {
}
}
29. 靜態與非靜態final常量不能在catch塊中初始化
靜態與非靜態塊中如果丟擲了異常,則一定要使用try-catch塊來捕獲。
public class Test {
static final int i;
static {
try {
i = f();
} catch (RuntimeException e) {
i = 1;
}
}
static int f() {
throw new RuntimeException();
}
}
上面的程式編譯不能通過。表面上是可以的,因為i第一次初始化時可能丟擲異常,所以拋異常時可以在catch塊中初
始化,最終還是隻初始化一次,這正是空final所要求的,但為什麼編譯器不知道這些呢?
要確定一個程式是否不止一次地對一個空final進行賦值是很困難的問題。語言規範在這一點上採用了保守的方式。
30. System.exit()與finally
try {
System.out.println("Hello world");
System.exit(0);
// 或者使用Runtime退出系統
// Runtime.getRuntime().exit(0);
} finally {
System.out.println("Goodbyte world");
}
上面的程式會列印出"Goodbyte world"嗎?不會。
System.exit將立即停止所有的程式執行緒,它並不會使finally語句塊得到呼叫,但是它在停止VM之前會執行關閉掛鉤
操作(這此掛鉤操作是註冊到Runtime.addShutdownHook上的執行緒),這對於釋放VM之外的資源很有幫助。使用掛鉤程
序修改上面程式:
System.out.println("Hello world");
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
System.out.println("Goodbyte world");
}
});
System.exit(0);
另外,物件回收時,使用VM呼叫物件的finalize()方法有兩種:
System.runFinalization():該方法讓虛擬機器也只是盡最大努力去完成所有未執行的finalize()終止方法,但不一定
會執行。
System.runFinalizersOnExit(true):該方法一定會回收,但不安全,已被廢棄。因為它可能對正在使用的物件呼叫
終結方法,而其他執行緒同時正在操作這些物件,從而導致不正確的行為或死鎖。
為了加快垃圾回收,使用System.gc(),但不一定馬上執行加收動作,由虛擬機器決定,實質上是呼叫
Runtime.getRuntime().gc()。
System的很多方法都是呼叫Runtime類的相關方法來實現的。
31. 遞迴構造
public class S {
private S instance = new S();
public S() {}
}
如果在程式外面構造該類的例項,則會丟擲java.lang.StackOverflowError錯誤。其原因是例項變數的初始化操作將
先於構造器的程式體而執行。
32. 構造器中的異常
如果父類構造器丟擲了檢測異常,則子類也只能丟擲,而不能採用try-catch來捕獲:
public class P {
public P() throws Exception {}
}
class S extends P {
public S() throws Exception {
try {
// 不能在try塊中明確呼叫父類構造器,因為構造的
// 明確呼叫只能放在第一行
// !! super();
//try-catch不能捕獲到父類構造器所丟擲的異常,子類只能丟擲
} catch (Exception e) {
}
}
}
如果初使化例項屬性時丟擲了異常,則構造器只能丟擲異常,在構造器中捕獲不起作用:
public class A {
private String str = String.class.newInstance();
public A() throws InstantiationException, IllegalAccessException {}
public A(int i) throws Exception {
try {//即使這裡捕獲了,方法簽名還是得要丟擲
} catch (Exception e) {
}
}
/*
* !!編譯不能通過,因為str2為靜態的,他不能通過構造器來捕獲,所以只
* 能使用靜態方法來捕獲。即初始化靜態成員時不能丟擲捕獲性異常。
*/
//!!private static String str2 = String.class.newInstance();
// 只能使用靜態方法來捕獲異常,如果是丟擲的執行時異常則不需要捕獲
private static String str2 = newInstance();
private static String newInstance() throws RuntimeException {
try {
return String.class.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
33. StackOverflowError
Java虛擬機器對棧的深度限制到了某個值,當超過這個值時,VM就丟擲StackOverflowError。一般VM都將棧的深度限制
為1024,即當方法呼叫方法的層次超過1024時就會產生StackOverflowError。
類篇
34. 引數相容的方法過載
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArr) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
上面的程式列印的是“double array”,為什麼?
null可代表任何非基本型別物件。
Java的過載解析過程是分兩階段執行的。第一階段選取所有可獲得並且可應用的方法或構造器。第二階段在第一階段
選取的方法或構造器中選取最精確的一個。如果一個方法或構造器可以接受傳遞給另一個方法或構造器的任何引數,
那麼我們就說第一個方法比第二個方法缺乏精確性,呼叫時就會選取第二個方法。
使用上面的規則來解釋該程式:構造器Confusing(Object o)可以接受任何傳遞Confusing(double[] dArr)的引數,因
此Confusing(Object o)相對缺乏精確性,所以Confusing(null)會呼叫Confusing(double[] dArr)構造器。
如果想強制要求編譯器選擇一個自己想要的過載版本,需要將實參強制轉型為所需要的構造器或方法的引數型別:如
這裡要呼叫Confusing(Object o)本版,則這樣呼叫:Confusing((Object)null)。
如果你確實進行了過載,那麼請確保所有的過載版本所接受的引數型別都互不相容,這樣,任何兩個過載版本都不會
同時是可應用的。
35. 靜態方法不具有多型特性
class A1 {
public static void f() {
System.out.println("A1.f()");
}
}
class A2 extends A1 {
public static void f() {
System.out.println("A2.f()");
}
}
class T {
public static void main(String[] args) {
A1 a1 = new A1();
A1 a2 = new A2();
// 靜態方法不具有多型效果,它是根據引用宣告型別來呼叫
a1.f();// A1.f()
a2.f();// A1.f()
}
}
對靜態方法的呼叫不存在任何動態的分派機制。當一個程式呼叫了一個靜態方法時,要被呼叫的方法都是在編譯時就
被選定的,即呼叫哪個方法是根據該引用被宣告的型別決定的。上面程式中a1與a2引用的型別都是A1型別,所以呼叫
的是A1中的f()方法。
36. 屬性只能被隱藏
class P {
public String name = "P";
}
class S extends P {
// 隱藏父類的name域,而不像方法屬於重寫
private String name = "S";
}
public class Test {
public static void main(String[] args) {
// !! S.name is not visible
// !! System.out.println(new S().name);
// 屬性不能被重寫,只是被隱藏,所以不具有多型性為
System.out.println(((P) new S()).name);// p
}
}
屬性的呼叫與靜態方式的呼叫一樣,只與前面引用型別相關,與具體的例項沒有任何關係。
當你在宣告一個域、一個靜態方法或一個巢狀型別時,如果其名與基類中相對應的某個可訪問的域、方法或型別相同
時,就會發生隱藏。
37. 屬性對巢狀類的遮掩
class X {
static class Y {
static String Z = "Black";
}
static C Y = new C();
}
class C {
String Z = "White";
}
public class T {
public static void main(String[] args) {
System.out.println(X.Y.Z);// White
System.out.println(((X.Y) null).Z);// Black
}
}
當一個變數和一個型別具有相同的名字,並且它們位於相同的作用域時,變數名具有優先權。變數名將遮掩型別名。
相似地,變數名和型別名可以遮掩包名。
38. 不能重寫不同包中的defualt訪問許可權方法
package click;
public class P {
public void f() {
//因為子類沒有重寫該方法,所以呼叫的還是父類中的方法
prt();
}
void prt() {
System.out.println("P");
}
}
package hack;
import click.P;
public class T {
private static class S extends P {
// 這裡沒有重寫父類的方法,因為父類方法不可見
void prt() {
System.out.println("S");
}
}
public static void main(String[] args) {
new S().f();// P
}
}
一個包內私有(default)的方法不能被位於另一個包中的某個方法直接重寫。
關於私有方法,還有屬效能否被重寫,請參考 方法能覆寫(重寫),屬效能覆寫嗎?
39. 重寫、隱藏、過載、遮蔽、遮掩
重寫:一個例項方法可以重寫在其超類中可訪問到的具有相同簽名的所有例項方法,從而能動態分派,換句話說,VM
將基於例項的執行期型別來選擇要呼叫的重寫方法。重寫是物件導向程式設計技術的基礎。
public class P{
public void f(){}
}
class S extends P{
public void f(){}//重寫
}
重寫時異常要求:
l 如果父類方法丟擲的是捕獲型異常,則子類也只能丟擲同類的捕獲型異常或其子類,或不丟擲。
l 父類丟擲捕獲型異常,子類卻丟擲執行時異常,這是可以,因為丟擲執行時就相當於沒有丟擲任何異常。
l 如果父類丟擲的是非捕獲型異常,則子類可以丟擲任意的非捕獲型異常,沒有擴大異常範圍這一問題。
l 如果父類丟擲的是非捕獲異常,子類也可以不用丟擲,這與父類為捕獲型異常是一樣的。
l 如果父類丟擲的是非捕獲異常,子類就不能丟擲任何捕獲型異常,因為這樣會擴大異常的範圍。
返回型別的協變:從Java SE5開始子類方法可以返回比它重寫的基類方法更具體的型別,但是這在早先的Java版本是
不允許——重寫時子類的返回型別一定要與基類相同。但要注意的是:子類方法返回型別要是父類方法返回型別的子
類,而不能反過來。
方法引數型別協變:如果父子類同名方法的引數型別為父子關係,則為引數型別協變,此時不屬於重寫,而是方法的
過載,以前版本就是這樣。
如果父類的方法為private時,子類同名的方法的方法名前可以使用任何修飾符來修飾。我們可以隨意地新增一個新的
私有成員(方法、域、型別),或都是修改和刪除一箇舊的私有成員,而不需要擔心對該類的客戶造成任何損害。換
而言之,私有成員被包含它們的類完全封裝了。
父與子類相同簽名方法不能一靜一動的,即父類的方法是靜態的,而子類不是,或子類是靜態的,而父類不是,編譯
時都不會通過。
父與子相同簽名方法都是靜態的方法時,方法名前的修飾符與非靜態方法重寫的規則一樣,但不屬於重寫,因為靜態
方法根本就不具有多型性。
最後,屬於成員也不具有多型特性,相同名的域屬於隱藏,而不管域前面的修飾符為什麼:
class P {
public static final String str = "P";
}
class S extends P {
//編譯能通過。可以是final,這裡屬於隱藏
public static final String str = "S";
public static void main(String[] args) {
System.out.println(S.str);//s
}
}
隱藏:一個域、靜態方法或成員型別可以分別隱藏在其超類中可訪問到的具有相同名字(對方法而言就是相同的方法
簽名)的所有域、靜態方法或成員型別。隱藏一個成員將阻止其被繼承。
public class P{
public static void f(){}
}
class S extends P{
//隱藏,不會繼承P.f()
public static void f(){}
}
過載:在某個類中的方法可以過載另一個方法,只要它們具有相同的名字和不同的簽名。由呼叫所指定的過載方法是
在編譯期選定的。
public class T{
public static void f(int i){}
public static void f(String str){}//過載
}
遮蔽:一個變數、方法或型別可以分別遮蔽在一個閉合的文字範圍內的具有相同名字的所有變數、方法或型別。如果
一個實體被遮蔽了,那麼你用它的簡單名是無法引用到它的;根據實體的不同,有時你根本就無法引用到它。
public class T {
private static String str = "feild";
public static void main(String[] args) {
String str = "local";// 遮蔽
System.out.println(str);// local
// 可以通過適當的方式來指定
System.out.println(T.str);// feild
}
}
public class T {
private final int size;
// 引數屬於方法區域性範圍類變數,遮蔽了同名成員變數
public T(int size) {
//使用適當的引用來指定
this.size = size;
}
}
遮掩:一個變數可以遮掩具有相同名字的一個型別,只要它們都在同一個範圍內:如果這個名字被用於變數與型別都
被許可的範圍,那麼它將引用到變數上。相似地,一個變數或一個型別可以遮掩一個包。遮掩是唯一一種兩個名字位
於不同的名字空間的名字重用形式,這些名字空間包括:變數、包、方法或型別。如果一個型別或一個包被遮掩了,
那麼你不能通過其簡單名引用到它,除非是在這樣一個上下文環境中,即語法只允許在其名字空間中出現一種名字:
public class T {
static String System;
public static void main(String[] args) {
// !!不能編譯,遮掩 java.lang.System
// !! System.out.println("Hello");
// 可明確指定
java.lang.System.out.println("Hello");
}
}
40. 構造器中靜態常量的引用問題
class T {
// 先於靜態常量t初始化,固可以在構造器中正常使用
private static final int y = getY();
/*
* 嚴格按照靜態常量宣告的先後順來初始化:即t初始
* 化完後,才初始化後面的靜態常量j,所以構造器中
* 引用後面的靜態常量j時,會是0,即記憶體清零時的值
*/
public static final T t = new T();
// 後於靜態常量t初始化,不能在構造器中正常使用
private static final int j = getJ();
private final int i;
static int getY() {
return 2;
}
static int getJ() {
return 2;
}
// 單例
private T() {
i = y - j - 1;
//為什麼j不是2
System.out.println("y=" + y + " j=" + j);// y=2 j=0
}
public int getI() {
return i;
}
public static void main(String[] args) {
System.out.println(T.t.getI());// 1
System.out.println(T.j);// 2
}
}
該程式所遇到的問題是由類初始化順序中的迴圈而引起的:初始化t時需呼叫建構函式,而呼叫建構函式前需初始化所
有靜態成員,此時又包括對t的再次初始化。
T類的初始化是由虛擬機器對main方法的呼叫而觸發的。首先,其靜態域被設定預設值,其中y、j被初始化為0,而t被初始化為null。接下來,靜態域初始器按照其宣告的順序執行賦值動作。第一個靜態域是y,它的值是通過呼叫getY獲取的,賦值操作完後結果為2。第一個初始化完成後,再進行第二個靜態域的賦值操作,第二個靜態域為t,它的值是通過呼叫T()建構函式來完成的。這個構造器會用二個涉及靜態域y、j來初始化非靜態域i。通常,讀取一個靜態域是會引起一個類被初始化,但是我們又已經在初始化T類。JavaVM規範對遞迴的初始化嘗試會直接被忽略掉(按理來說在建立出例項前需初始化完所有的靜態域後再來建立例項),這樣就導致在靜態域被初始化之前就呼叫了構造器,後面的靜態域j將得不到正常的初始化前就在構造器中被使用了,使用時的值為記憶體分配清零時的,即0。當t初始化完後,再初始化j,此時j得到的值為2,但此時對i的初始化過程來說已經晚了。
在final型別的靜態域被初始化之前,存在著讀取其值的可能,而此時該靜態域包含的還只是其所屬型別的預設值。這
是與直覺想違背的,因為我們通常會將final型別的域看作是常量,但final型別的域只有在其初始化表示式是字面常
量表示式時才是真正的常量。
再看看另一程式:
class T {
private final static int i = getJ();
private final static int j;
static {
j = 2;
}
static int getJ() {
return j;
}
public static void main(String[] args) {
System.out.println(T.j);// 2
/*
* 因為上面的語句已經初使完T類,所以下面語句是
* 不 會 再引起類的初始化,這裡的結果用的是第一
* 次( 即上面語句)的初始化結果
*/
System.out.println(T.i);// 0
}
}
為什麼第二個輸出是0而不是2呢?這就是因為VM是嚴格按照你宣告的順序來初始化靜態域的,所以前面的引用後面的靜態域時,基本型別就是0,引用型別就會是null。
所以要記住:靜態域,甚至是final型別的靜態域,可能會在它們被初始化之前,被讀走其預設值。
另,類初始化規則請參考《惰性初始化》一節
41. instanceof與轉型
System.out.println(null instanceof String);//false
System.out.println(new Object() instanceof String);//false
//編譯能通過
System.out.println((Object) new Date() instanceof String);//false
//!!程式不具有實際意義,但編譯時不能通過
//!!System.out.println(new Date() instanceof String);
//!!執行時拋ClassCastException,這個程式沒有任何意義,但可以編譯
//!!System.out.println((Date) new Object());
null可以表示任何引用型別,但是instanceof操作符被定義為在其左運算元為null時返回false。
如果instanceof告訴你一個物件引用是某個特定型別的例項,那麼你就可以將其轉型為該型別,並呼叫該型別的方法
,而不用擔心會丟擲ClassCastException或NullPointerException異常。
instanceof操作符有這樣的要求:左運算元要是一個物件的或引用,右運算元是一個引用型別,並且這兩個運算元的
型別是要父子關係(左是右的子類,或右是左的子類都行),否則編譯時就會出錯。
42. 父類構造器呼叫已重寫的方法
public class P {
private int x, y;
private String name;
P(int x, int y) {
this.x = x;
this.y = y;
// 這裡實質上是呼叫子類被重寫的方法
name = makeName();
}
protected String makeName() {
return "[" + x + "," + y + "]";
}
public String toString() {
return name;
}
}
class S extends P {
private String color;
S(int x, int y, String color) {
super(x, y);
this.color = color;
}
protected String makeName() {
return super.makeName() + ":" + color;
}
public static void main(String[] args) {
System.out.println(new S(1, 2, "red"));// [1,2]:null
}
}
在一個構造器呼叫一個已經被其子類重寫了的方法時,可能會出問題:如果子類重寫的方法要訪問的子類的域還未初
始化,因為這種方式被呼叫的方法總是在例項初始化之前執行。要想避免這個問題,就千萬不要在父類構造器中呼叫
已重寫的方法。
43. 靜態域與靜態塊的初始順序
public class T {
public static int i = prt();
public static int y = 1;
public static int prt() {
return y;
}
public static void main(String[] args) {
System.out.println(T.i);// 0
}
}
上面的結果不是1,而是0,為什麼?
類初始化是按照靜態域或靜態塊在原始碼中出現的順序去執行這些靜態初始器的(即誰先定義,就先初始化誰),上現程式中由於i先於y宣告,所以先初始化i,但由於i初始化時需要由y來決定,此時y又未初始化,實為初始前的值0,所以i的最後結果為0。
44. 請使用引用型別呼叫靜態方法
public class Null {
public static void greet() {
System.out.println("Hello world!");
}
public static void main(String[] args) {
((Null) null).greet();
}
}
上面程式執行時不會列印NullPointerException異常,而是輸出"Hello world!",關鍵原因是:呼叫靜態方法時將忽略前面的呼叫物件或表達示,只與物件或表示式計算結果的型別有關。
在呼叫靜態方法時,一定要使用類去呼叫,或是靜態匯入後直接使用。
45. 迴圈中的不能宣告區域性變數
for (int i = 0; i < 1; i++)
Object o ; //!! 編譯不能通過
for (int i = 0; i < 1; i++)
Object o = new Object(); //!! 編譯不能通過
一個本地變數宣告看起來像是一條語句,但是從技術上來說不是。
Java語言規範不允許一個本地變數宣告語句作為一條語句在for、while或do迴圈中重複執行。
一個本地變數宣告作為一條語句只能直接出現在一個語句塊中(一個語句塊是由一對花 括號以及包含在這對花括號中的語句和宣告構成的):
for (int i = 0; i < 1; i++) {
Object o = new Object(); // 編譯OK
}
46. 內部類反射
public class Outer {
public class Inner {
public String toString() {
return "Hello world";
}
}
public void getInner() {
try {
// 普通方式建立內部類例項
System.out.println(new Outer().new Inner());// Hello world
//!! 反射建立內部類,拋異常:java.lang.InstantiationException:Outer$Inner
System.out.println(Inner.class.newInstance());
} catch (Exception e) {
}
}
public static void main(String[] args) {
new Outer().getInner();
}
}
上面因為構造內部類時外部類例項不存在而拋異常。
一個非靜態的巢狀類的構造器,在編譯的時候會將一個隱藏的引數作為它的第一個引數,這個參數列示它的直接外圍例項。如果使用反射建立內部類,則要傳遞個隱藏引數的唯一方法就是使用java.lang.reflect.Constructor:
Constructor c = Inner.class.getConstructor(Outer.class);//獲取帶引數的內部類建構函式
System.out.println(c.newInstance(Outer.this));//反射時還需傳進外圍類
應用篇
47. 不可變的引用型別
BigInteger total = BigInteger.ZERO;
total.add(new BigInteger("1"));
total.add(new BigInteger("10"));
System.out.println(total);//0
上面程式的結果為11嗎?答案是0。
BigInteger例項是不可變的。String、BigDecimal以及包裝型別:Integer、Long、Short、Byte、Character、Boolean、Float和Double也是如此。對這些型別的操作將返回新的例項。
不可變型別更容易設計、實現與作用;它們出錯的可能性更小,並且更加安全。
本程式修改如下:
BigInteger total = BigInteger.ZERO;
total=total.add(new BigInteger("1"));
total=total.add(new BigInteger("10"));
System.out.println(total);//11
48. 請同時重寫equals()與hashCode()
class T {
private String str;
T(String str) {
this.str = str;
}
public boolean equals(Object obj) {
if(!(obj instanceof T)){
return false;
}
T t = (T)obj;
return t.equals(this.str);
}
public static void main(String[] args) {
Set set = new HashSet();
set.add(new T("str"));
System.out.println(set.contains(new T("str")));//false
}
}
上面的程式不會列印true,而是false,為什麼?
hashCode約定要求相等的物件要具有相同的雜湊碼。
無論何時,只要你重寫了equals方法,你就必須同時重寫hashCode方法。
如果將自定的型別物件放入HashSet、HashMap、Hashtable、LinkedHashSet、LinkedHashMap這此雜湊集合時,一定需要重寫equals與hashCode方法,這樣在放入進去之後還能查詢出來。如果放入其他非雜湊型別的集合時,其實只需要
重寫equals就可以了。
本程式解決辦法重寫hashCode()方法:
public int hashCode() {
return 37 * this.str.hashCode();
}
49. 日期設定
Calendar c = Calendar.getInstance();
c.set(2010, 12, 31);// 月是從0開始的,11其實表示12月
System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH));
c = Calendar.getInstance();
c.set(2010, 11, 31);
System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH));
本程式較簡單,只需注意月是從0開始的就可以了,如果你設定月為12,則會自動轉換為下一年。
50. IdentityHashMap
class T {
private String str;
T(String str) {
this.str = str;
}
public int hashCode() {
return 37 * this.str.hashCode();
}
public boolean equals(Object obj) {
return this.str.equals(((T) obj).str);
}
public static void put(Map m) {
m.put("str", "1");
/*
* 由於上面程式將 "str" 放入了字串常量池,
* 所以str是同一個物件,不管是什麼樣型別的
* Map,即使使用IdentityHashMap都只放入一次
*/
m.put("str", "2");
m.put(new T("str"), "3");
m.put(new T("str"), "4");
}
public static void main(String[] args) {
Map m = new HashMap();
put(m);
System.out.println(m.size());// 2
//IdentityHashMap比較時使用==替換equals()方法
m = new IdentityHashMap();
put(m);
System.out.println(m.size());// 3
}
}
51. 靜態匯入的優先權
import static java.util.Arrays.toString;
import java.util.Arrays;
public class T {
public static void main(String[] args) {
prt(1, 2, 3);
}
static void prt(Object... args) {
// 自身繼承至Object類的toString的優先順序高於靜態匯入的方法
//!! System.out.println(toString(args));//不能編譯
System.out.println(Arrays.toString(args));
}
}
本身就屬於某個範圍的成員在該範圍內與靜態匯入相比具有優先權。
52. PrintStream對輸出結果的緩衝
public static void main(String[] args) {
String str = "Hello World";
for (int i = 0; i < str.length(); i++) {
System.out.write(str.charAt(i));
}
}
上面的程式沒有輸出結果。
這裡的問題在於System.out是帶有緩衝的。輸出的結果被寫入了System.out的緩衝區,但是緩衝區從來都沒有被重新整理。大多數人認為,當有輸出產生的時候System.out和System.err會自動地進位制重新整理,但這並不完全正確,這兩個流都屬於PrintStream型別,請看API DOC描述:一個PrintStream被建立為自動重新整理,這意味著當一個位元組陣列(byte[])被寫入、或者某個println方法被呼叫、或者一個換行字元或位元組('/n')被寫入之後,PrintStream型別的flush方法就會被自動呼叫。
令人奇怪的是,如果這個程式用print(char)去替代write(int),它就會重新整理System.out並輸出結果,這種行為與print(char)的文件是矛盾的,因為其文件敘述道:“列印一個字元,這個字元將根據平臺預設的字元編碼方式翻譯成一個或多個位元組,並且這些位元組將完全按照write(int)方法的方式輸出。”,但這裡沒有換行符卻也自動的重新整理了。
類似的,如果程式改用print(String),它也會對流進行重新整理。所以呼叫print方法也是會自動重新整理的。
53. 呼叫作業系統命令時被阻塞問題
public static void main(String[] args) throws IOException,
InterruptedException {
String command = "java ProcessTest exc";
if (args.length != 0) {
for (int i = 0; i < 200; i++) {
System.out.println(command);
System.err.println(command);
}
} else {
Process process = Runtime.getRuntime().exec(command);
int exitValue = process.waitFor();
System.out.println("exit value = " + exitValue);
}
}
執行java ProcessTest發現程式阻塞。
Process文件描述:由於某些本地平臺只提供有限大小的緩衝,所以如果不能迅速地讀取子程式的輸出流,就有可能會導致子程式的阻塞,甚至是死鎖。這恰好就是這裡所發生的事情:沒有足夠的緩衝空間來儲存這些輸出結果。為了結子程式(Process執行緒),父程式(Main執行緒)必須排空它的輸出流(標準流與錯誤流都需要排空),即要去快取中讀取結果:
static void readResult(final InputStream is) {
new Thread(new Runnable() {
public void run() {
try {
// 排空快取內容
while (is.read() >= 0);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
然後在process.waitFor()之前加上
readResult(process.getErrorStream());
readResult(process.getInputStream());
即可輸出exit value = 0。
另外,只能根據process.waitFor返回的結果來判斷作業系統命令執行是否成功(成功:0,失敗:1),我們不能根據
錯誤流中是否有內容來判斷是否執行成功。
54. 實現Serializable的單例問題
class Dog implements Serializable{
public static final Dog INSTANCE = new Dog();
private Dog(){}
}
上面能控制只生成一個單例項嗎?
如果對實現了Serializable的物件進行序列化後,再反序列化,內中會不只一個例項了,因為反序列化時會重新生成一個物件。
既然INSTANCE為靜態域,那序列化時返回的物件如果也是INSTANCE就可以解決問題了,而開啟API我們發現Serializable介面確實有這樣兩個特殊的方法描述:
l 將物件寫入流時需要指定要使用的替代物件的可序列化類,應使用準確的簽名來實現此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplace 方法將由序列化呼叫,前提是如果此方法存在,而且它可以通過被序列化物件的類中定義的一個方法訪問。因此,該方法可以擁有私有 (private)、受保護的 (protected) 和包私有 (package-private) 訪問。子類對此方法的訪問遵循 java 訪問規則。
l 在從流中讀取類的一個例項時需要指定替代的類應使用的準確簽名來實現此特殊方法:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
此 readResolve 方法遵循與 writeReplace 相同的呼叫規則和訪問規則。
上述兩個方法的只要出現,就會履蓋以下兩個方法(這兩個方法本質的意義就是用來替換序列與反序列的物件),雖然會執行它們,但最後得到的結果卻是writeReplace、readResolve兩個方法寫入或讀出的物件:
l private void writeObject(java.io.ObjectOutputStream out) throws IOException
l private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;
另外,writeObject與readObject需成對實現,而writeReplace與readResolve則不需要成對出現,一般單獨使用。如果同時出現這四個方法,最後寫入與讀出的結果以writeReplace和readResolve方法的結果為準。
所以下要解決真真單例項問題,我們如下修正:
class Dog implements Serializable {
public static final Dog INSTANCE = new Dog();
private Dog() {}
private Object readResolve() {
return INSTANCE;
}
}
public class SerialDog {
public static void main(String[] args) throws IOException,
ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(Dog.INSTANCE);
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
Dog dog = (Dog) new ObjectInputStream(bin).readObject();
System.out.println(dog == Dog.INSTANCE);//true
}
}
一個實現了Serializable的單例類,必須有一個readResolve方法,用以返回它的唯一例項。
55. thread. isInterrupted()與Thread.interrupted()
public class SelfInerruption {
public static void main(String[] args) {
Thread.currentThread().interrupt();
if (Thread.interrupted()) {
// Interruped:false
System.out.println("Interruped:" + Thread.interrupted());
} else {
System.out.println("Not interruped:" + Thread.interrupted());
}
}
}
上面結果走的是第一個分支,但結果卻不是Interruped:true?
Thread.interrupted()為Thread的靜態方法,呼叫它首先會返回當前執行緒的中斷狀態(如果當前執行緒上呼叫了interrupt()方法,則返回true,否則為false),然後再清除當前執行緒的中斷狀態,即將中斷狀態設定為false。換句話說,如果連續兩次呼叫該方法,則第二次呼叫將返回 false。
而isInterrupted()方法為例項方法,測試執行緒是否已經中斷,並不會清除當前執行緒中斷狀態。
所以這裡應該使用isInterrupted()例項方法,就可以修復該問題。
56. 惰性初始化
public class Lazy {
private static boolean initial = false;
static {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("befor...");//此句會輸出
/*
* 由於使用Lazy.initial靜態成員,又因為Lazy還未 初
* 始化完成,所以該執行緒會在這裡等待主執行緒初始化完成
*/
initial = true;
System.out.println("after...");//此句不會輸出
}
});
t.start();
try {
t.join();// 主執行緒等待t執行緒結束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println(initial);
}
}
看看上面變態的程式,一個靜態變數的初始化由靜態塊裡的執行緒來初始化,最後的結果怎樣?
當一個執行緒訪問一個類的某個成員的時候,它會去檢查這個類是否已經被初始化,在這一過程中會有以下四種情況:
1、 這個類尚未被初始化
2、 這個類正在被當前執行緒初始化:這是對初始化的遞迴請求,會直接忽略掉(另,請參考《構造器中靜態常量的引用問題》一節)
3、 這個類正在被其他執行緒而不是當前執行緒初始化:需等待其他執行緒初始化完成再使用類的Class物件,而不會兩個執行緒都會去初始化一遍(如果這樣,那不類會初始化兩遍,這顯示不合理)
4、 這個類已經被初始化
當主執行緒呼叫Lazy.main,它會檢查Lazy類是否已經被初始化。此時它並沒有被初始化(情況1),所以主執行緒會記錄下當前正在進行的初始化,並開始對這個類進行初始化。這個過程是:主執行緒會將initial的值設為false,然後在靜態塊中建立並啟動一個初始化initial的執行緒t,該執行緒的run方法會將initial設為true,然後主執行緒會等待t執行緒執行完畢,此時,問題就來了。
由於t執行緒將Lazy.initial設為true之前,它也會去檢查Lazy類是否已經被初始化。這時,這個類正在被另外一個執行緒(mian執行緒)進行初始化(情況3)。在這種情況下,當前執行緒,也就是t執行緒,會等待Class物件直到初始化完成,可惜的是,那個正在進行初始化工作的main執行緒,也正在等待t執行緒的執行結束。因為這兩個執行緒現在正相互等待,形成了死鎖。
修正這個程式的方法就是讓主執行緒在等待執行緒前就完成初始化操作:
public class Lazy {
private static boolean initial = false;
static Thread t = new Thread(new Runnable() {
public void run() {
initial = true;
}
});
static {
t.start();
}
public static void main(String[] args) {
// 讓Lazy類初始化完成後再呼叫join方法
try {
t.join();// 主執行緒等待t執行緒結束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(initial);
}
}
雖然修正了該程式掛起問題,但如果還有另一執行緒要訪問Lazy的initial時,則還是很有可能不等initial最後賦值就被使用了。
總之,在類的初始化期間等待某個執行緒很可能會造成死鎖,要讓類初始化的動作序列儘可能地簡單。
57. 繼承內部類
一般地,要想例項化一個內部類,如類Inner1,需要提供一個外圍類的例項給構造器。一般情況下,它是隱式地傳遞給內部類的構造器,但是它也是可以以 expression.super(args) 的方式即通過呼叫超類的構造器顯式的傳遞。
public class Outer {
class Inner1 extends Outer{
Inner1(){
super();
}
}
class Inner2 extends Inner1{
Inner2(){
Outer.this.super();
}
Inner2(Outer outer){
outer.super();
}
}
}
class WithInner {
class Inner {}
}
class InheritInner extends WithInner.Inner {
// ! InheritInner() {} // 不能編譯
/*
* 這裡的super指InheritInner類的父類WithInner.Inner的預設建構函式,而不是
* WithInner的父類建構函式,這種特殊的語法只在繼承一個非靜態內部類時才用到,
* 表示繼承非靜態內部類時,外圍物件一定要存在,並且只能在 第一行呼叫,而且一
* 定要呼叫一下。為什麼不能直接使用 super()或不直接寫出呢?最主要原因就是每個
* 非靜態的內部類都會與一個外圍類例項對應,這個外圍類例項是執行時傳到內
* 部類裡去的,所以在內部類裡可以直接使用那個物件(比如Outer.this),但這裡
* 是在外部內外 ,使用時還是需要存在外圍類例項物件,所以這裡就顯示的通過構造
* 器傳遞進來,並且在外圍物件上顯示的呼叫一下內部類的構造器,這樣就確保了在
* 繼承至一個類部類的情況下 ,外圍物件一類會存在的約束。
*/
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
58. Hash集合序列化問題
class Super implements Serializable{
// HashSet要放置在父類中會百分百機率出現
// 放置到子類中就不一定會出現問題了
final Set set = new HashSet();
}
class Sub extends Super {
private int id;
public Sub(int id) {
this.id = id;
set.add(this);
}
public int hashCode() {
return id;
}
public boolean equals(Object o) {
return (o instanceof Sub) && (id == ((Sub) o).id);
}
}
public class SerialKiller {
public static void main(String[] args) throws Exception {
Sub sb = new Sub(888);
System.out.println(sb.set.contains(sb));// true
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(sb);
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
sb = (Sub) new ObjectInputStream(bin).readObject();
System.out.println(sb.set.contains(sb));// false
}
}
Hash一類集合都實現了序列化的writeObject()與readObject()方法。這裡錯誤原因是由HashSet的readObject方法引起的。在某些情況下,這個方法會間接地呼叫某個未初始化物件的被覆寫的方法。為了組裝正在反序列化的HashSet,HashSet.readObject呼叫了HashMap.put方法,而put方法會去呼叫鍵的hashCode方法。由於整個物件圖正在被反序列
化,並沒有什麼可以保證每個鍵在它的hashCode方法被呼叫時已經被完全初始化了,因為HashSet是在父類中定義的,而在序列化HashSet時子類還沒有開始初始化(這裡應該是序列化)子類,所以這就造成了在父類中呼叫還沒有初始完成(此時id為0)的被子類覆寫的hashCode方法,導致該物件重新放入hash表格的位置與反序列化前不一樣了。hashCode返回了錯誤的值,相應的鍵值對條目將會放入錯誤的單元格中,當id被初始化為888時,一切都太遲了。
這個程式的說明,包含了HashMap的readObject方法的序列化系統總體上違背了不能從類的構造器或偽構造器(如序列化的readObject)中呼叫可覆寫方法的規則。
如果一個HashSet、Hashtable或HashMap被序列化,那麼請確認它們的內容沒有直接或間接地引用它們自身,即正在被序列化的物件。
另外,在readObject或readResolve方法中,請避免直接或間接地在正在進行反序列化的物件上呼叫任何方法,因為正在反序列化的物件處於不穩定狀態。
59. 迷惑的內部類
public class Twisted {
private final String name;
Twisted(String name) {
this.name = name;
}
// 私有的不能被繼承,但能被內部類直接訪問
private String name() {
return name;
}
private void reproduce() {
new Twisted("reproduce") {
void printName() {
// name()為外部類的,因為沒有被繼承過來
System.out.println(name());// main
}
}.printName();
}
public static void main(String[] args) {
new Twisted("main").reproduce();
}
}
在頂層的型別中,即本例中的Twisted類,所有的本地的、內部的、巢狀的長匿名的類都可以毫無限制地訪問彼此的成員。
另一個原因是私有的不能被繼承。
60. 編譯期常量表示式
第一個PrintWords代表客戶端,第二個Words代表一個類庫:
class PrintWords {
public static void main(String[] args) {
System.out//引用常量變數
.println(Words.FIRST + " "
+ Words.SECOND + " "
+ Words.THIRD);
}
}
class Words {
// 常量變數
public static final String FIRST = "the";
// 非常量變數
public static final String SECOND = null;
// 常量變數
public static final String THIRD = "set";
}
現在假設你像下面這樣改變了那個庫類並且重新編譯了這個類,但並不重新編譯客戶端的程式PrintWords:
class Words {
public static final String FIRST = "physics";
public static final String SECOND = "chemistry";
public static final String THIRD = "biology";
}
此時,端的程式會列印出什麼呢?結果是 the chemistry set,不是the null set,也不是physics chemistry biology,為什麼?原因就是 null不是一個編譯期常量表示式,而其他兩個都是。
對於常量變數(如上面Words類中的FIRST、THIRD)的引用(如在PrintWords類中對Words.FIRST、Words.THIRD的引用)會在編譯期被轉換為它們所表示的常量的值(即PrintWords類中的Words.FIRST、Words.THIRD引用會替換成"the"與"set")。
一個常量變數(如上面Words類中的FIRST、THIRD)的定義是,一個在編譯期被常量表示式(即編譯期常量表示式)初
始化的final的原生型別或String型別的變數。
那什麼是“編譯期常量表示式”?精確定義在[JLS 15.28]中可以找到,這樣要說的是null不是一個編譯期常量表示式。
由於常量變數會編譯進客戶端,API的設計者在設計一個常量域之前應該仔細考慮一下是否應該定義成常量變數。
如果你使用了一個非常量的表示式去初始化一個域,甚至是一個final或,那麼這個域就不是一個常量。下面你可以通過將一個常量表示式傳給一個方法使用得它變成一個非常量:
class Words {
// 以下都成非常量變數
public static final String FIRST = ident("the");
public static final String SECOND = ident(null);
public static final String THIRD = ident("set");
private static String ident(String s) {
return s;
}
}
總之,常量變數將會被編譯進那些引用它們的類中。一個常量變數就是任何常量表示式初始化的原生型別或字串變數。且null不是一個常量表示式。
61. 打亂陣列
class Shuffle {
private static Random rd = new Random();
public static void shuffle(Object[] a) {
for (int i = 0; i < a.length; i++) {
swap(a, i, rd.nextInt(a.length));
}
}
public static void swap(Object[] a, int i, int j) {
Object tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
public static void main(String[] args) {
Map map = new TreeMap();
for (int i = 0; i < 9; i++) {
map.put(i, 0);
}
// 測試陣列上的每個位置放置的元素是否等概率
for (int i = 0; i < 10000; i++) {
Integer[] intArr = new Integer[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
shuffle(intArr);
for (int j = 0; j < 9; j++) {
map.put(j,(Integer)map.get(j)+intArr[j]);
}
}
System.out.println(map);
for (int i = 0; i < 9; i++) {
map.put(i,(Integer) map.get(i)/10000f);
}
System.out.println(map);
}
}
上面的演算法不是很等概率的讓某個元素打亂到其位置,程式執行了多次,大致的結果為:
{0=36031, 1=38094, 2=39347, 3=40264, 4=41374, 5=41648, 6=41780, 7=41188, 8=40274}
{0=3.6031, 1=3.8094, 2=3.9347, 3=4.0264, 4=4.1374, 5=4.1648, 6=4.178, 7=4.1188, 8=4.0274}
如果某個位置上等概率出現這9個值的話,則平均值會趨近於4,但測試的結果表明:開始的時候比較低,然後增長超過了平均值,最後又降下來了。
如果改用下面演算法:
public static void shuffle(Object[] a) {
for (int i = 0; i < a.length; i++) {
swap(a, i, i + rd.nextInt(a.length - i));
}
}
多次測試的結果大致如下:
{0=40207, 1=40398, 2=40179, 3=39766, 4=39735, 5=39710, 6=40074, 7=39871, 8=40060}
{0=4.0207, 1=4.0398, 2=4.0179, 3=3.9766, 4=3.9735, 5=3.971, 6=4.0074, 7=3.9871, 8=4.006}
所以修改後的演算法是合理的。
另一種打亂集合的方式是通過Api中的Collections工具類:
public static void shuffle(Object[] a) {
Collections.shuffle(Arrays.asList(a));
}
其實演算法與上面的基本相似,當然我們使用API中提供的會更好,會在效率上獲得最大的受益。