改善Java程序的151個建議--記錄(持續更新)

原書下載地址:http://download.csdn.net/detail/hua245942641/9272199

建議7: 警惕自增的陷阱

自增有兩種形式,分別是i++和++i,i++ 表示的是先賦值後加1,++i 是先加1後賦值。而對於下面的代碼:

public class Client {
    public static void main(String[] args) {
        int count =0;
        for(int i=0;i<10;i++){
            count=count++;
        }
        System.out.println("count="+count);
    }
}

這個程序輸出的count 等於幾?是count 自加10 次嗎?答案等於10 ?可以非常肯定地告訴你,答案錯誤!運行結果是count 等於0。爲什麼呢?
count++ 是一個表達式,是有返回值的,它的返回值就是count 自加前的值,Java 對自加是這樣處理的:首先把count 的值(注意是值,不是引用)拷貝到一個臨時變量區,然後對count 變量加1,最後返回臨時變量區的值。程序第一次循環時的詳細處理步驟如下:
步驟1 JVM 把count 值(其值是0)拷貝到臨時變量區。
步驟2 count 值加1,這時候count 的值是1。
步驟3 返回臨時變量區的值,注意這個值是0,沒修改過。
步驟4 返回值賦值給count,此時count 值被重置成0。
“count=count++”這條語句可以按照如下代碼來理解:

public static int mockAdd(int count){
    // 先保存初始值
    int temp =count;
    // 做自增操作
    count = count+1;
    // 返回原始值
    return temp;
}

於是第一次循環後count 的值還是0,其他9 次的循環也是一樣的,最終你會發現count的值始終沒有改變,仍然保持着最初的狀態。

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

instanceof 是一個簡單的二元操作符,它是用來判斷一個對象是否是一個類實例的,其操作類似於>=、==,非常簡單,我們來看段程序,代碼如下:

public class Client {
    public static void main(String[] args) {
        //String 對象是否是Object 的實例
        boolean b1 = "Sting" instanceof Object;
        //String 對象是否是String 的實例
        boolean b2 = new String() instanceof String;
        //Object 對象是否是String 的實例
        boolean b3 = new Object() instanceof String;
        // 拆箱類型是否是裝箱類型的實例
        boolean b4 = 'A' instanceof Character;
        // 空對象是否是String 的實例
        boolean b5 = null instanceof String;
        // 類型轉換後的空對象是否是String 的實例
        boolean b6 = (String)null instanceof String;
        //Date 對象是否是String 的實例
        boolean b7 = new Date() instanceof String;
        // 在泛型類中判斷String 對象是否是Date 的實例
        boolean b8 = new GenericClass<String>().isDateInstance("");
    }
}
class GenericClass<T>{
        // 判斷是否是Date 類型
    public boolean isDateInstance(T t){
        return t instanceof Date;
    }
}

就這麼一段程序,instanceof 的所有應用場景都出現了,同時問題也產生了:這段程序中哪些語句會編譯通不過?我們一個一個地來解說。

1、”Sting” instanceof Object
返回值是true,這很正常,“String” 是一個字符串,字符串又繼承了Object,那當然是返回true 了。

2、new String() instanceof String
返回值是true,沒有任何問題,一個類的對象當然是它的實例了。

3、new Object() instanceof String
返回值是false,Object 是父類,其對象當然不是String 類的實例了。要注意的是,這句話其實完全可以編譯通過,只要instanceof 關鍵字的左右兩個操作數有繼承或實現關係,就可以編譯通過。

4、’A’ instanceof Character
這句話可能有讀者會猜錯,事實上它編譯不通過,爲什麼呢?因爲’A’ 是一個char 類型,也就是一個基本類型,不是一個對象,instanceof只能用於對象的判斷,不能用於基本類型的判斷。

5、null instanceof String
返回值是false, 這是instanceof 特有的規則: 若左操作數是null, 結果就直接返回false,不再運算右操作數是什麼類。這對我們的程序非常有利,在使用instanceof 操作符時,不用關心被判斷的類(也就是左操作數)是否爲null,這與我們經常用到的equals、toString方法不同。

6、(String)null instanceof String
返回值是false,不要看這裏有個強制類型轉換就認爲結果是true,不是的,null 是一個萬用類型,也可以說它沒類型,即使做類型轉換還是個null。

7、new Date() instanceof String
編譯通不過,因爲Date 類和String 沒有繼承或實現關係,所以在編譯時直接就報錯了,instanceof 操作符的左右操作數必須有繼承或實現關係,否則編譯會失敗。

8、new GenericClass().isDateInstance(“”)
編譯通不過?非也,編譯通過了,返回值是false,T 是個String 類型,與Date之間沒有繼承或實現關係,爲什麼”t instanceof Date” 會編譯通過呢?那是因爲Java 的泛型是爲編碼服務的,在編譯成字節碼時,T 已經是Object 類型了,傳遞的實參是String 類型,也就是說T 的表面類型是Object,實際類型是String,那”t instanceof Date” 這句話就等價於Object instance of Date” 了,所以返回false 就很正常了。

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

這個建議主要是針對被final修飾的constant常量。
對於final 修飾的基本類型和String 類型,編譯器會認爲它是穩定態(Immutable Status),所以在編譯時就直接把值編譯到字節碼中了,避免了在運行期引用(Run-timeReference),以提高代碼的執行效率。類在編譯時,字節碼中就寫上了這個常量,而不是一個地址引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client 類,輸出還是照舊。
而對於final 修飾的類(即非基本類型),編譯器認爲它是不穩定態(Mutable Status),在編譯時建立的則是引用關係(該類型也叫做Soft Final),如果Client 類引入的常量是一個類或實例,即使不重新編譯也會輸出最新值。

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

這個建議主要是因爲在做奇偶性判斷時,一般會用取餘的方式來進行。
如果要判斷n的奇偶性,最好使用n%2==0 來做判斷依據,而不要使用n%2 == 1來進行。因爲在java中的取餘操作的模擬算法如下:

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

當輸入-1 的時候,運算結果是-1,當然不等於1 了,所以它就被判定爲偶數了。因此最好使用n%2==0 來做判斷依據

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

在計算機中浮點數有可能(注意是可能)是不準確的,它只能無限接近準確值,而不能完全精確。爲什麼會如此呢?這是由浮點數的存儲規則所決定的,我們先來看0.4這個十進制小數如何轉換成二進制小數,使用“乘2 取整,順序排列”法,我們發現0.4 不能使用二進制準確的表示,在二進制數世界裏它是一個無限循環的小數,也就是說,“展示”都不能“展示”,更別說是在內存中存儲了(浮點數的存儲包括三部分:符號位、指數位、尾數),可以這樣理解,在十進制的世界裏沒有辦法準確表示1/3,那在二進制世界裏當然也無法準確表示1/5,在二進制的世界裏1/5 是一個無限循環小數。

要解決此問題有兩種方法:
1、使用BigDecimal
BigDecimal 是專門爲彌補浮點數無法精確計算的缺憾而設計的類,並且它本身也提供了加減乘除的常用數學算法。特別是與數據庫Decimal類型的字段映射時,BigDecimal 是最優的解決方案。

2、使用整型
把參與運算的值擴大100 倍,並轉變爲整型,然後在展現時再縮小100 倍,這樣處理的好處是計算簡單、準確,一般在非金融行業(如零售行業)應用較多。此方法還會用於某些零售POS 機,它們的輸入和輸出全部是整數,那運算就更簡單。

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

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

Java 是先運算然後再進行類型轉換的,具體地說就是因爲disc2 的三個運算參數都是int 類型,三者相乘的結果雖然也是int 類型,但是已經超過了int 的最大值,所以其值就是負值了(爲什麼是負值?因爲過界了就會從頭開始),再轉換成long 型,結果還是負值。
解決起來也很簡單,只要加個小小的“L” 即可,代碼如下:

long dis2 = LIGHT_SPEED * 60L * 8;

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

四捨五入,小於5 的數字被捨去,大於等於5 的數字進位後捨去,由於所有位上的數字都是自然計算出來的,按照概率計算可知,被舍入的數字均勻分佈在0 到9 之間,下面以10 筆存款利息計算作爲模型,以銀行家的身份來思考這個算法。
1、四舍:捨棄的數值:0.000、0.001、0.002、0.003、0.004,因爲是捨棄的,對銀行家來說,就不用付款給儲戶了,那每捨棄一個數字就會賺取相應的金額:0.000、0.001、0.002、0.003、0.004。
2、五入:進位的數值:0.005、0.006、0.007、0.008、0.009,因爲是進位,對銀行家來說,每進一位就會多付款給儲戶,也就是虧損了,那虧損部分就是其對應的10 進制補數:0.005、0.004、0.003、0.002、0.001。
因爲捨棄和進位的數字是在0 到9 之間均勻分佈的,所以對於銀行家來說,每10 筆存款的利息因採用四捨五入而獲得的盈利是:
0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = -0.005
也就是說,每10 筆的利息計算中就損失0.005 元,即每筆利息計算損失0.0005 元。

這個算法誤差是由美國銀行家發現的,並且對此提出了一個修正算法,叫做銀行家舍入(Banker’s Round)的近似算法,其規則如下:
1、捨去位的數值小於5 時,直接捨去;
2、捨去位的數值大於等於6 時,進位後捨去;
3、當捨去位的數值等於5 時,分兩種情況:5 後面還有其他數字(非0),則進位後捨去;若5 後面是0(即5 是最後一個數字),則根據5 前一位數的奇偶性來判斷是否需要進位,奇數進位,偶數捨去。

以上規則彙總成一句話:四捨六入五考慮,五後非零就進一,五後爲零看奇偶,五前爲偶應捨去,五前爲奇要進一。我們舉例說明,取2 位精度:

round(10.5551) = 10.56
round(10.555) = 10.56
round(10.545) = 10.54

目前Java 支持以下七種舍入方式:
1、ROUND_UP: 遠離零方向舍入。
向遠離0 的方向舍入,也就是說,向絕對值最大的方向舍入,只要捨棄位非0 即進位。
2、ROUND_DOWN:趨向零方向舍入。
向0 方向靠攏,也就是說,向絕對值最小的方向輸入,注意:所有的位都捨棄,不存在進位情況。
3、ROUND_CEILING:向正無窮方向舍入。
向正最大方向靠攏,如果是正數,舍入行爲類似於ROUND_UP ;如果爲負數,則舍入行爲類似於ROUND_DOWN。注意:Math.round 方法使用的即爲此模式。
4、ROUND_FLOOR:向負無窮方向舍入。
向負無窮方向靠攏,如果是正數,則舍入行爲類似於 ROUND_DOWN ;如果是負數,則舍入行爲類似於 ROUND_UP。
5、HALF_UP: 最近數字舍入(5 進)。
這就是我們最最經典的四捨五入模式。
6、HALF_DOWN:最近數字舍入(5 舍)。
在四捨五入中,5 是進位的,而在HALF_DOWN 中卻是捨棄不進位。
7、HALF_EVEN :銀行家算法。

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

Java 引入包裝類型(Wrapper Types)是爲了解決基本類型的實例化問題,以便讓一個基本類型也能參與到面向對象的編程世界中。
基本類型和包裝類型都是可以通過自動裝箱(Autoboxing)和自動拆箱(AutoUnboxing)自由轉換的。而需要注意的是在自動拆箱的過程中,如果是對象爲null,那麼就會拋出NullPointException
我們謹記一點:包裝類型參與運算時,要做null值校驗。

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

基本類型是可以比較大小的,其所對應的包裝類型都實現了Comparable 接口也說明了此問題,那我們來比較一下兩個包裝類型的大小,代碼如下:

public class Client {
    public static void main(String[] args) {
        Integer i = new Integer(100);
        Integer j = new Integer(100);
        compare(i,j);
    }
    // 比較兩個包裝對象大小
    public static void compare(Integer i , Integer j) {
        System.out.println(i == j);
        System.out.println(i > j);
        System.out.println(i < j);
    }
}

代碼很簡單,產生了兩個Integer 對象,然後比較兩者的大小關係,既然基本類型和包裝類型是可以自由轉換的,那上面的代碼是不是就可打印出兩個相等的值呢?讓事實說話,運行結果如下:

false
false
false

竟然是3 個false,也就是說兩個值之間不等,也沒大小關係,這也太奇怪了吧。不奇怪,我們來一一解釋。
1、i == j
在Java 中“==”是用來判斷兩個操作數是否有相等關係的,如果是基本類型則判斷值是否相等,如果是對象則判斷是否是一個對象的兩個引用,也就是地址是否相等,這裏很明顯是兩個對象,兩個地址,不可能相等。
2、i > j 和 i < j
在Java 中,“>”和“<”用來判斷兩個數字類型的大小關係,注意只能是數字型的判斷,對於Integer 包裝類型,是根據其intValue() 方法的返回值(也就是其相應的基本類型)進行比較的(其他包裝類型是根據相應的value 值來比較的,如doubleValue、floatValue 等),那很顯然,兩者不可能有大小關係的。
總的來說,在這個問題中,i==j比較的是地址,肯定不相等,而> 或者 < 比較的是兩個對象的xxxValue的返回值,是相等的,所以結果反倒是false了。

建議28: 優先使用整型池

java在Integer這個對象中存在一組從-128到127的整數池。
要實例出一個Integer對象,可以採用new和valueOf方法這兩種方式:
1、new Integer(i),這樣生成的Integer對象,每次地址都是不一樣的。
2、使用Integer.valueOf(i)生成的對象,如果i的範圍在-128到127之間,那麼不管調用多少次這個方法得到的都是同一個對象,也就是說用==來比較是相等的。這是因爲Integer.valueOf方法導致的。方法如下:

static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
    for(int i = 0; i < cache.length; i++)
    cache[i] = new Integer(i - 128);
}

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

cache 是IntegerCache 內部類的一個靜態數組,容納的是- 128 到127 之間的Integer 對象。通過valueOf 產生包裝對象時,如果int 參數在- 128 和127 之間,則直接從整型池中獲得對象,不在該範圍的int 類型則通過new 生成包裝對象。
整型池的存在不僅僅提高了系統性能,同時也節約了內存空間,這也是我們使用整型池的原因,也就是在聲明包裝對象的時候使用valueOf 生成,而不是通過構造函數來生成的原因。順便提醒大家,在判斷對象是否相等的時候,最好是用equals 方法,避免用“==”產生非預期結果。

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