給程式設計師的幾點程式設計經驗----《編寫高質量程式碼》

路飛的影子發表於2020-04-05

一.在非idea編譯的情況下,不要只替換一個類

我們經常在系統中定義一個常量介面(或常量類),以囊括系統中涉及的常量,從而簡化程式碼,方便開發,在很多的開源專案中採用了類似的方法,比如在Struts2中,org.apache.struts2.StrutsConstants就是一個常量類,它定義了一個Status框架中配置的有關的常量,而org.apache.status2.StatusStatics則是一個常量介面,其中定義了一個OGNL訪問的關鍵字。

    關於常量介面(類)我們來看一個例子,首先定義一個常量類:

public class Constant{
    //定義人類壽命的極限
    public final static int MAX_AGE = 150;
}

這是一個非常簡單的常量類,定義了一個人類的最大年齡,我們引用這個常量,程式碼如下:

public class Client {
    public static void main(String[] args){
        System.out.println("人類的極限壽命:"+Constant.MAX_AGE);
    }    
}

執行的結果非常簡單。目前的程式碼編寫都是在“智慧型”IDE工具中完成的,下面我們暫時回溯到原始時代,也就是迴歸到記事本編寫程式碼的時代,然後看看會發生什麼奇妙的事情把。

修改常量類Constant類,人類的壽命增加了,最大能活到180歲,程式碼如下:

public class Constant {
    //定義人類的極限壽命
    public final static int MAX_AGE = 180;
}

然後重新編譯:javac Constant, 編寫完成後執行:java Client,大家想看看輸出的極限年齡是多少碼?

輸出的結果是:“人類壽命極限是:150”,竟然沒有改變為180,太奇怪了,這是為何?原因是:對於final修飾的基本資料型別和String型別,編譯器會認為它是穩定態(Immutable Status),所以在編譯時就直接把值編譯到位元組碼中了,避免了在執行期引用(Run-time Referece),以提高程式碼的執行效率。針對我們的雷子來說,Client類在編譯時,位元組碼中就寫不上“150”這個常量,而不是一個地址的引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client類,輸出還是照舊。

   而對於final修飾的類(即非基本型別),編譯器認為它是不穩定態(Mutable Status),在編譯時建立的則是引用關係(該型別也叫做Soft Final),如果Client類引入的常量是一個類或者例項,即使不重新編譯也會輸出最新值。

呃,我們例子中為什麼不在IDE工具中執行呢?那是因為在IDE中不能重現該問題,若修改了Constant類,IDE工具會自動編譯多有的引用類,“智慧”化遮蔽了該問題,但潛在的風險其實仍然存在。

注意: 釋出應用系統時禁止使用類檔案替換的方式,整體war包釋出才是萬全之策。

二.用偶判斷,不用奇判斷

  判斷一個數是奇數還是小學裡學的基本知識,能夠被2整除的整數是偶數,不能被2整除的是奇數,這規則簡單又明瞭,還有什麼好考慮?好,我們來看一個例子,程式碼如下:

public class Client {
    public static void main(String[] args){
        //接受鍵盤輸入引數
        Scanner input = new Scanner(System.in);
        System.out.println("請輸入多個數字判斷奇偶:");
        Where(input.hasNextInt()){
            int i = input.nextInt();
            String str = i+ "->" + (i%2 == 1?"奇數":"偶數");
            Syetem.out.println(str);
        }
    }
}

輸入多個數字,然後判斷每個數字的奇偶性,不能被2整除就是奇數,其他的都是偶數,完全是根據奇偶數的定義編寫的程式,我們看看列印的結果:

     前三個還是很靠譜,第四個引數-1怎麼可能會是偶數呢,這Java也太差勁了,如此簡單的計算也會出錯!我們先了解一下Java中的取餘(%標示符)演算法,模擬程式碼如下:

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

看到這段程式碼,相信大家都會心地笑了,原來Java是這麼處理取餘計算的呀。根據上面的模擬取餘可知,當輸入-1的時候,運算結果是-1,當然不等於1了,所以它就是被判斷為偶數了,也就是說我們的判斷失誤了。問題明白了,修正也是很簡單,改為判斷是否是偶數即可,程式碼如下:

注意: 對於基礎知識,我們應該“知其然,並知其所以然”。

三.邊界,邊界,還是邊界

某商家生產的電子產品非常暢銷,需要提前30天預定才能搶到手,同時它還規定了一個會員可擁有的最多產品數量,目的是防止囤積壓貨肆意加價。會員的預定過程是這樣的:先登入官方網站,選擇產品型號,然後設定需要預定的數量,提交,符合規則即下單成功,不符合規則提示下單失敗。後臺的處理邏輯模擬如下:

public class Client {
    //一個會員擁有產品的最多數量
    public final static int LIMIT = 2000;
    public static void main(String[] args){
        //會員當前擁有的產品數量
        int cur =  1000;
        Scanner input = new Scanner(System.in);
        System.out.println("請輸入需要預定的數量:");
        Where(input.hasNext()){
            int order = input.nextInt();
            //當前擁有的與準備預定的產品數量之和
            if(order>0 && order+cur<=LIMIT){
                System.out.println("你已經成功預定的"+order+"個產品!");
            }else{
                System.out.println("超過限額,預定失敗!");    
            }
        }
    } 
}

       這是一個簡易的訂單處理程式,其中cur代表的是會員已經擁有的產品數量,LIMIT是一個會員最多擁有的產品數量,如果當前預定數量與擁有數量之和超過了最大數量,則預定失敗,否則下單成功。業務邏輯很簡單,同時在web介面上對訂單數量做了嚴格的校驗,比如不能是負值、不能超過最大數量等,但是人算不如天算,執行不大倆小時資料庫中就出現異常:某會員擁有的產品數量與預定數量之和遠遠大於限額。我們模擬一下:

看到沒,這個數字遠遠超過2000的限額,但是竟然預定成功了,真實神奇!

看著2147483647這個數字很眼熟?那就對了,它是int型別的最大值,沒錯,有人輸入一個最大值,是校驗條件失效了,why?我們來看程式,order的值是2147483647,那在加上1000就超出int的範圍了,其結果是-2147482649,那當然是小於正數2000了!一句話可歸原因:數字超界使校驗條件失效。

     在單元測試中,有一項測試叫做邊界測試(也有交臨界值測試),如果一個方法接受的int型別的引數,那麼以下三個值是必測的:0、最大值、最小值,其中正最大和負最小是邊界值,如果這三個值都沒有問題,方法才是比較安全可靠的。我們的例子就是因為缺少邊界測試,導致生產系統產生了嚴重的偏差。

四.優先使用整形池

看程式碼我們解決問題,

public static void main(String[] args){
    Scanner in =  new Scanner(System.in);
    while(in.hasNextInt()){
    
    int input = in.nextInt();
    System.out.println("\n-----"+ii+"的相等判斷-----");
    //倆個通過new 產生的物件
    Integer i = new Integer(input);
    Integer j = new Integer(input);
    
    System.out.println("new產生的物件:"+(i==j));

    //基本型別轉換成包裝型別後比較
    i=input;
    j=input;
    
    System.out.println("基本型別轉換的物件:"+(i==j));
    //通過靜態方法生成一個例項
    i=Integer.valueOf(input);
    j=Integer.valueOf(input);

    System.out.println("valueOf產生的物件:"+(i==j));
    }
}

 

分別輸入三個值,127,128,555產生的結果圖:

很不可思議,對吧。那這是為什麼呢?

(1)new產生的Integer物件

new宣告就是要生成一個新的物件,沒二話,這是倆個物件,地址肯定不相等。false

(2)裝箱生成的物件

對於這一點,首先要說明的是裝箱動作是通過valueOf方法實現的,也就是說後倆個演算法是相同的,那結果肯定也是一樣。那現在的問題是:valueOf是如何生成物件的呢?我們閱讀一下Integer.valueOf的實現程式碼。

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

 

顯而易見,如果是-128到127之前的int型別轉換成Integer物件,則直接從cache陣列中取,那這個cache資料是作甚麼?

static final Integer cache[] = new Integer[-(-128)+127+1];

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

cache是IntegerCache內部類的一個靜態陣列,容納的是-127到128之間的Integer物件。通過valueOf產生包裝物件時,如果int引數在-128到127之間,則直接在從整型池中獲取物件,不再該範圍內的int 型別則通過new 生成包裝物件。

總結:通過包裝類的valueOf生成包裝例項可以顯著提高空間和時間效能。

相關文章