最近準備重新把《Java解惑》看一遍,爲了以後可以快速的回憶起書中說到的一些陷阱,我把認爲值得記下來的簡要記錄一下,以備以後看。
畢竟一天是看不完的,所以本博客是持續更新的.......
1. 想通過num%2==1判斷num是不是奇數是有問題的,因爲對於負奇數會等於-1而不是1。所以可以通過num%2!=0來判斷是否爲奇數。爲了提高性能可以採用(num&1)!=0來判斷是否爲奇數。
2. 二進制數是無法準確表達所有小數的,比如說0.9。在需要精確答案的地方避免使用float和double;儘量使用int,long,BigDecimal。
3. JAVA不具有目標確定類型的特性,比如說: long num=50000*50000; 執行之後num並不等於2500000000。因爲JAVA並不能根據結果是long型的而在計算中採用long型數進行計算。實際上50000*50000的計算是按照int型計算的。所以應該這樣:long num=50000L*50000;
4. 十六進制數和八進制數和十進制數是不同的,如果十六進制或八進制數的最高位被置位了,那麼他們就是負數。而十進制數是通過加一個減號實現負數的。所以對於十六進制和八進制數要特別注意。對於0xcafebabe被提升爲long型數值是會發生符號擴展變爲0xffffffffcafebabeL。
5. 從較窄的整數轉換成較寬的整數時候有一條規則:如果最初的數值類型是有符號的,那麼就執行符號擴展;如果是char,那麼不管它將要被轉換成什麼類型都執行零擴展。
6. 在單個表達式中更不要對相同的變量賦值兩次。
7. 確定條件表達式結果類型的規則;1:如果第二個和第三個操作數類型相同那麼它既是條件表達式的類型。2:如果一個操作數的類型是T,T表示byte,short,char。而另一個操作數是一個int型的常量表達式,他的值也可以用類型T表示,那麼條件表達式的類型就是T;若不能被T表示,比如說超出了範圍,比如:
char x='a';
System.out.println(true ? 50000 : x );
//char無法表示50000,所以結果是:
?
3:否則對類型進行二進制提升。條件表達式的類型就是提升後的類型。
8. 複合賦值表達式自動將所執行計算的結果轉換爲其左側變量的類型。比如:
short x=0;
int num=12345;
x+=i;
System.out.println(x);
//計算結果會轉型成short,由於short無法表示12345所以結果爲:
-7616
所以請不要將複合賦值操作符作用於byte,short,char類型的變量。
9. 當且僅當+操作符的操作數中至少有一個是String類型時,纔會執行字符串連接操作。 比如:
String str='a'+'a'+"";
String string=""+'a'+'a';
System.out.println(str);
System.out.println(string);
//運行結果:
194
aa
10. 對所有數組調用toString()的結果就是 巴拉巴拉hashCode神馬的。還有就是如果引用爲null,調用toString會轉換成“null”。(我在這裏喫過苦頭)。比如:
String str=null;
String string="hello";
str=str+string;
System.out.println(str);
//運行結果:
nullhello
要想講一個char數組轉換成一個字符串調用toString()可不行,要調用String.valueOf(char[])比如:
char arr[]={'a','v','c'};
System.out.println(String.valueOf(arr));
//運行結果:
avc
11. String類型的編譯期常量是內存限定的,也就是說任何兩個String類型的常量表達式,如果指定的是相同的字符序列,那麼他們就用同一對象引用來表示。如:
String string="hello";
String str="hello";
System.out.println(str==string);
//運行結果:
true
而以下也是:
String string="hello";
String str="he"+"llo";
System.out.println(str==string);
//運行結果:
true
但是如果改爲:
String string="hello2";
String str="hello"+"he".length();
System.out.println(str==string);
//結果:
false
好好體會一下。
再注意一個問題:+的優先級比==高,所以:
if ("hello"+"java"=="hellojava") {
System.out.println("OK");
}
//結果:
OK
12. 我們來看一下Unicode轉義字符,Java對再字符串字面常量中的Unicode轉義字符沒有提供任何特殊處理,會直接將其轉化爲相應字符,比如:
System.out.println("hello\u0022.length());
//\u0022被直接轉成了"所以,相當於"hello".length()
5
想在字符串常量表達式中用一些特殊符號怎麼辦呢?可以使用轉義字符序列,就是在特殊字符前加上\,舉例如下:
System.out.println("hello\\".length());
System.out.println("\\\\".length());
System.out.println("\"".length());
//結果:
6
2
1
Unicode轉義字符帶來的混亂還不止是這些,比如如果我們在註釋中這樣寫:
// \u
編譯器是一定會報錯的,因爲\u表示一個Unicode轉義字符的開始,而且Unicode轉義字符必須是良構的,在註釋中也必須是。所以要在\u後跟四個數字(16進制大小寫均可)。Unicode轉義字符是很危險的,比如\u000A會轉義成換行。
13. 將byte數組轉化爲String的時候,我們都在使用一個字符集。無論是否指定。所以最好這樣做:String str=new String(bytes,"ISO-8859-1");我們指定字符集。
缺省的會使用OS的缺省字符集。
14. 塊註釋符(/* */)是不支持嵌套的。比如下面語句就會出錯:
/*
if("+-*/".equals("hello"))
*/
所以註釋掉代碼的最好方式是使用單行的註釋。//
15. String.replaceAll接受了一個正則表達式做爲第一個參數。\後面跟非特殊字符是非法的。比如 \c就是編譯不通過的。我們應該使用String.replace。
16. Random.nextInt(int):返回一個僞隨機數,均等地分佈在從0(包含)到指定的數值(不包含)之間的一個int數值。StringBuffer有一個無參的構造函數,一個接受一個String做爲字符串緩衝區初始內容的構造器,以及一個接受int做爲緩衝區初始容量的構造器。new StringBuffer('a');實際上是計算‘a’的int型值然後開闢這麼大的緩衝區。
17. byte是有符號的。((byte)0x90==0x90)會返回false。
18. for(int i=Integer.MIN_VALUE; i<=Integer.MAX_VALUE; i++)是個無限循環。因爲i遞增到Integer.MAX_VALUE的時候再遞增就變成Integer.MIN_VALUE。
19. 最近發現Java中的移位操作符的問題,詳見這篇博客:
http://wjy320.iteye.com/blog/2070199
20. 我們都知道,一個無窮大的數字加上1之後還是無窮大。事實上可以用double或者float數值來表示無窮大。例如:double i=1.0e40; (i==i+1)返回的true。
對於float類型,加1不會產生任何效果的最小級數是2的25次方。對於double類型,最小級數是2的54次方。還有請記住:將一個很小的浮點數加到一個很大的浮點數上時,將不會改變大浮點數的值。所以:二進制浮點數只是對實際算數的一種近似。
21. 知道0.0/0.0等於多少嗎?這肯定是有問題的不是嗎?因爲怎麼可能去除0呢?在Java中,這個特殊的數字是有名字的叫NaN。可以通過:
Float.NaN 和 Double.NaN獲得。要注意一下:1,NaN不等於任何浮點數包括它自身(NaN!=NaN)。2,任何浮點操作,只要它的一個或者多個操作數爲NaN,那麼其結果爲NaN。
22. >>> 和 <<<是對應於無符號移位操作的操作符。之前我們提過對於複合操作符在計算的時候會執行拓寬原始類型的操作(如果兩個操作數不一樣長的話,會將短的操作數執行拓寬操作(對於有符號數執行符號擴展,對於char永遠執行零擴展。))。
23. 注意一下比較操作符(<,>,>=,<=)。左邊這些比較操作無論什麼時候都是進行的數值比較。但是==操作可就不同了,對於==而言必須至少有一個操作數是原生類型時才執行數值比較,否則執行的是判斷是否引用相等的操作。比如:
Integer i=new Integer(3);
Integer j=new Integer(3);
System.out.println(i==j);
System.out.println(i>=j);
System.out.println(i<=j);
//運行結果:
false
true
true
24. java使用2的補碼的算術運算是不對稱的。對於每一種有符號的整數類型(int,long,byte和short),負的數值總是比正的數值多一個,這個多出來的數值總是這種類型所能表示的最小的數值。(比如int表示的數據範圍是負的2的32次方到正的2的32次方減1)。對Integer.MIN_VALUE取負數還等於他自身。對Short,Byte...同樣適用。
25. 不要使用浮點數做爲循環索引,將一個int或者long轉換成一個float或double時,可能會丟失精度。事實上float雖然是32位的,但是隻能提供24位的精度,指數神馬的還要佔位的。
26. 取餘操作和乘法操作具有相同的優先級,所以ms%60*1000相當於(ms%60)*1000。
27. 千萬不要時間用return,break,continue或者throw來退出finally語句塊,千萬不要允許讓受檢驗的異常傳播到finally語句塊之外。finally中直接寫break或continue是不合法的。比如:finally{break;} 因爲break和continue必須在循環中,finally並不是循環。一般在打開IO流的時候我們在finally中關閉它,像這樣:
finally{
if(in!=null) in.close();
if(out!=null) out.close();
}
其實以上寫法是不好的,close可能拋出IOException異常,如果in.close()拋出了異常會導致out.close()永遠不會執行到。從而輸出流永遠保持開放。所以正確的寫法應該是:
finally{
if(in!=null){
try {
in.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
if(out!=null){
try {
out.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
}
從java5.0開始,可以使用Closeable接口進行重構:
private static void closeIgnoringException(Closeable c){
if(c!=null){
try {
c.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
finally{
closeIgnoringException(in);
closeIgnoringException(out);
}
28. 1,如果一個catch子句要捕獲一個類型爲E的受檢查異常,而其相對應的try子句不能拋出E的某種子類型的異常,那麼這就是一個編譯期錯誤;但是捕獲Exception或者Throwable的catch子句是永遠合法的,無論對應的try子句內容爲何。2,一個方法必須要麼捕獲其方法體可以拋出的所有受檢驗的異常,要麼聲明它將拋出這些異常。3,一個方法可以拋出的受檢查的異常集合是它所適用的所有類型聲明要拋出的受檢查異常集合的交集。比如:
interface A{
void f() throws CloneNotSupportedException;
}
interface B{
void f() throws InterruptedException;
}
interface C extends A,B{
}
public class Test implements C{
public static void main(String args[]){
C c=new Test();
c.f();
}
@Override
public void f(){
// TODO Auto-generated method stub
System.out.println("HELLO");
}
}
就是說接口C的方法f()會拋出A和B中方法f()的拋出異常的交集。所以接口C中的方法f()不會拋出異常。
29. 一個空的final域只有在它的確未被附過值的地方纔可以被賦值。比如下面是不合法的:
//錯誤原因:編譯器會認爲可能對USER_ID重複賦值,try中賦值語句後面的語句若拋出異常,會在catch中重複賦值。
private static final int USER_ID;
static{
try {
USER_ID=1;
} catch (Exception e) {
USER_ID=2;
// TODO: handle exception
}
}
//其實下面的也是錯的,因爲不能保證USER_ID一定被賦值成功。而final是必須被初始化的。
private static final int USER_ID;
static{
try {
USER_ID=1;
} catch (Exception e) {
// TODO: handle exception
}
}
解決辦法,加個方法就OK了:
private static final int USER_ID=getUserId();
private static int getUserId(){
try {
return 1;
} catch (Exception e) {
return 2;
// TODO: handle exception
}
}
30. 下面的finally中的代碼永遠都執行不到:
public static void main(String args[]){
try {
System.exit(0);
} finally{
// TODO: handle exception
System.out.println("Good bye.");
}
}
是不是感覺有悖常理,其實System.exit方法將停止當前線程和所有其他當場死亡的線程。finally子句並不能給予線程繼續執行的特殊權限。
31. 實例(不管是靜態的還是非靜態的)初始化操作是先於構造器的程序體而運行的。實例初始化操作拋出的任何異常都會傳播給構造器。如果初始化操作拋出的是受檢驗的異常,那麼構造器必須聲明也會拋出這些異常,但是應該避免這樣做。看看下面的程序,會產生棧溢出:
public class Test{
private Test instance=new Test();
public static void main(String args[]){
Test test=new Test();
}
}
//這就是沒有將構造函數私有化的惡果。
32. &操作符除了常見的做爲整形操作數的位AND操作符之外,當用於布爾操作數時,他的功能被重載爲邏輯AND操作符,但是他和常用的條件操作符&&有很大的不同。&總是要計算他的兩個操作數,而&&在其左邊的操作數被計算爲false時,就不在計算右邊的操作數了。|也是一樣的,|總是要計算他的兩個操作符,而||在其左邊的操作數被計算爲true時,就不再計算右邊的操作數了。
33. Class的成員變量如果是final的,要求必須對其初始化。其實將其的初始化延遲到構造函數中也是可以的。像下面這樣也可以:
private final int num=getNum();
public int getNum(){
return 1;
}
34. 1.一定要意識到Class.newInstance可以拋出它沒有聲明過的受檢查的異常。2.泛型信息是在編譯期而非運行期檢查的。3.Java的異常檢查機制並不是虛擬機強制執行的,它只是一個編譯期工具,被設計用來幫助我們更加容易的編寫正確的程序,但是在運行期可以繞過它。所以,不要忽視編譯器給出的警告信息。
35. 要是想檢測一個類是否丟失的程序,應該使用反射:
try {
Object mObject=Class.forName("ClassName").newInstance();
} catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
不要對捕獲NoClassDefFoundError形成依賴,類的初始化的時機是很明確的,但是類被加載的時機是不可預測的,捕獲Error及其子類型幾乎是不應該的。
36. 謎題45沒看。待補充。
下部分:http://wjy320.iteye.com/blog/2076381