1. 程式人生 > >編寫高質量程式碼(從入坑到出坑)

編寫高質量程式碼(從入坑到出坑)

第一坑:三元操作符的型別務必一致

    三元操作符是if-else的簡化寫法,在專案中使用它的地方很多,也非常好用,但是好用
又簡單的東西並不表示就可以隨便用,我們來看看下面這段程式碼:
public class Client i
    public static void main(String[] args){
                int i=80;
              String s = String.valueOf(i<100?90;100);
              String s1 = String.valueOf(i<l00?90;100.0);
            System.out.println(“兩者足否相等;".s.equals(s1));
    }


    分析一下這段程式:i是80,那它當然小幹100,兩者的返回值肯定都是90,再轉成
String型別,其值也絕對相等.毋庸置疑的。恩,分析得有點道理,但是變數s中三元操作
符的第二個運算元是100,而s1的第二個運算元是100.0,難道沒有影響嗎?不可能有影響
吧,三元操作符的條件都為真了,只返回第一個值嘛,與第二個值有一毛錢的關係嗎?貌似
有道理。
    果真如此嗎?我們通過結果來驗證一下,執行結果是:“兩者是否相等: false",什麼?
不相等,Why?

    問題就出在了100和100.0這兩個數字上,在變數s中,三元操作符中的第一個運算元
(90)和第二個運算元(100)都是int型別,型別相同,返回的結果也就是int型別的90,
而變數s1的情況就有點不同了,第一個運算元是90 ( int型別),第二個運算元卻是100.0,
而這是個浮點數,也就是說兩個運算元的型別不一致,可三元操作符必須要返回一個數據,

而且型別要確定,不可能條件為真時返回int型別,條件為假時返回float型別,編譯器是不
允許如此的,所以它就會進行型別轉換了,int型轉換為浮點數90.0,也就是說三元操作符的
返回值是浮點數90.0,那這當然與整型的90不相等了。這裡可能有讀者疑感了:為什麼是
整型轉為浮點,而不是浮點轉為整型呢?這就涉及三元操作符型別的轉換規則:
    1:若兩個運算元不可轉換,則不做轉換,返回值為Object型別。
    2:若兩個運算元是明確型別的表示式(比如變數),則按照正常的二進位制數字來轉換.
      int型別轉換為long型別,long型別轉換為float型別等。
    3:若兩個運算元中有一個是數字S,另外一個是表示式,且其型別標示為T,那麼.若
      數字S在T的範圍內,則轉換為T型別;若S超出了T型別的範圍,則T轉換為S
      型別(可以參考“建議22",會對該問題進行展開描述).
    4:若兩個運算元都是直接量數字(Literal) ,則返回值型別為範圍較大者.
    知道是什麼原因了,相應的解決辦法也就有了:保證三元操作符中的兩個運算元型別一
致,即可減少可能錯誤的發生。

第二坑:警惕自增的陷阱

記得大學剛開始學C語言時,老師就說:自增有兩種形式,分別是i++和++i, i++表

示的是先賦值後加1, ++i是先加1後賦值,這樣理解了很多年也沒出現向題,直到遇到如下
程式碼,我才懷疑我的理解是不是錯了:

public class Client{
      public static void main(String[] args)
int count =0;
for(int i=0;1<10;1++){
            count=count++;
}
Syetem.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
的值始終沒有改變,仍然保持著最初的狀態。
    此例中程式碼作者的本意是希望count自增,所以想當然地認為斌值給自身就成了,不
曾想掉到Java自增的陷阱中了.解決方法很簡單,只要把“count=count++"修改為
"count++”即可。該問題在不同的語言環境有不同的實現:C++中“count=count++”與
"count++”是等效的,而在PHP中則保持著與Java相同的處理方式.每種語言對自增的實
現方式各不同,讀者有興趣可以多找幾種語言測試一下,思考一下原理。
    下次如果看到某人T恤上印著“i=i++”,千萬不要鄙視他,記住,能夠以不同的語言解
釋清楚這句話的人絕對不簡單,應該表現出“如滔滔江水”般的敬仰,心理默唸清“高人,絕世高人“。

第三坑:避免用序列化類在建構函式中為不變數賦值

    我們知道帶有final標識的屬性是不變數,也就是說只能賦值一次,不能重複賦值,但
是在序列化類中就有點複雜了,比如有這樣一個類:

public class Person implements Sericlizable{

    private static final long serialVersionUID = 71282334L;

   //不變數

   public final String name = ""混世魔王";

}

    這個Person類(此時v1.0版本)被序列化,然後儲存在磁碟上,在反序列化時name
屬性會重新計算其值(這與static變數不同,static變數壓根就沒有儲存到資料流中),比如
name屬性修改成了“德天使”(版本升級為V2.0),那麼反序列化物件的name值就是“德
天使”。保持新舊物件的final變數相同,有利於程式碼業務邏輯統一,這是序列化的基本規則
之一,也就是說,如果final屬性是一個直接量,在反序列化時就會重新計算。對這基本規則
不多說,我們要說的是final變數另外一種賦值方式:通過建構函式賦值。程式碼如下:

這也是我們常用的一種賦值方式,可以把這個Person類定義為版本V1.0,然後進行序
列化,看看有什麼問題沒有,序列化的程式碼如下所示:

    Person的例項物件儲存到了磁碟上.它是一個貧血物件(承載業務屬性定義,但不
包含其行為定義),我們做一個簡單的模擬,修改一下name值代表變更,要注意的是

serialVersionUID保持不變,修改之後的程式碼如下:

    此時Person類的版本是V2.0,但serialVersionUlD沒有改變,仍然可以反序列化,其代
碼如下:

    現在問題來了:列印的結果是什麼?是混世魔王還是德天使?
    答案即將揭曉,答案是:混世魔王。
    final型別的變數不是會重新計算嗎?答案應該是“德天使”才對啊.為什麼會是“混世
魔王”?這是因為這裡觸及了反序列化的另一個規則:反序列化時建構函式不會執行。
    反序列化的執行過程是這樣的:JVM從資料流中獲取一個Object物件,然後根據資料
流中的類檔案描述資訊(在序列化時,儲存到磁碟的物件檔案中包含了類描述資訊,注意
是類描述資訊,不是類)檢視,發現是final變數,需要重新計算,於是引用Person類中的
name值,而此時JVM又發現name竟然沒有賦值,不能引用,於是它很“聰明”地不再初
始化,保持原值狀態,所以結果就是“混世魔王"了。
    讀者不要以為這樣的情況很少發生,如果使用Java開發過桌面應用,特別是參與過
對效能要求較高的專案(比如交易類專案),那麼很容易遇到這樣的問題。比如一個C/S
結構的線上外匯交易系統,要求提供24小時的聯機服務,如果在升級的類中有一個final
變最是建構函式賦值的,而且新舊版本還發生了變化,則在應用請求熱切的過程中(非
常短暫,可能只有30秒),很可能就會出現反序列化生成的final變數值與新產生的例項
值不相同的情況,於是業務異常就產生了,情況嚴重的話甚至會影響交易資料,那可是
天大的事故了。

注意在序列化類中,不使用建構函式為final變數賦值。

摘錄自:《編寫高質量程式碼:改善Java程式的152個建議》