程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 編寫高質量代碼:改善Java程序的151個建議(第1章:JAVA開發中通用的方法和准則___建議11~15),java151

編寫高質量代碼:改善Java程序的151個建議(第1章:JAVA開發中通用的方法和准則___建議11~15),java151

編輯:JAVA綜合教程

編寫高質量代碼:改善Java程序的151個建議(第1章:JAVA開發中通用的方法和准則___建議11~15),java151


建議11:養成良好習慣,顯示聲明UID

我們編寫一個實現了Serializable接口(序列化標志接口)的類,Eclipse馬上就會給一個黃色警告:需要添加一個Serial Version ID。為什麼要增加?他是怎麼計算出來的?有什麼用?下面就來解釋該問題。

  類實現Serializable接口的目的是為了可持久化,比如網絡傳輸或本地存儲,為系統的分布和異構部署提供先決條件支持。若沒有序列化,現在我們熟悉的遠程調用、對象數據庫都不可能存在,我們來看一個簡單的序列化類:

 1 import java.io.Serializable;
 2 public class Person implements Serializable {
 3     private String name;
 4 
 5     public String getName() {
 6         return name;
 7     }
 8 
 9     public void setName(String name) {
10         this.name = name;
11     }
12 
13 }

這是一個簡單的JavaBean,實現了Serializable接口,可以在網絡上傳輸,也可以在本地存儲然後讀取。這裡我們以java消息服務(Java Message Service)方式傳遞對象(即通過網絡傳遞一個對象),定義在消息隊列中的數據類型為ObjectMessage,首先定義一個消息的生產者(Producer),代碼如下:

1 public class Producer {
2     public static void main(String[] args) {
3         Person p = new Person();
4         p.setName("混世魔王");
5         // 序列化,保存到磁盤上
6         SerializationUtils.writeObject(p);
7     }
8 }

這裡引入了一個工具類SerializationUtils,其作用是對一個類進行序列化和反序列化,並存儲到硬盤上(模擬網絡傳輸),其代碼如下:

 1 import java.io.FileInputStream;
 2 import java.io.FileNotFoundException;
 3 import java.io.FileOutputStream;
 4 import java.io.IOException;
 5 import java.io.ObjectInputStream;
 6 import java.io.ObjectOutputStream;
 7 import java.io.Serializable;
 8 
 9 public class SerializationUtils {
10     private static String FILE_NAME = "c:/obj.bin";
11     //序列化
12     public static void writeObject(Serializable s) {
13         try {
14             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
15             oos.writeObject(s);
16             oos.close();
17         } catch (FileNotFoundException e) {
18             e.printStackTrace();
19         } catch (IOException e) {
20             e.printStackTrace();
21         }
22     }
23     //反序列化
24     public static Object readObject() {
25         Object obj = null;
26         try {
27             ObjectInputStream input = new ObjectInputStream(new FileInputStream(FILE_NAME));
28             obj=input.readObject();
29             input.close();
30         } catch (FileNotFoundException e) {
31             e.printStackTrace();
32         } catch (IOException e) {
33             e.printStackTrace();
34         } catch (ClassNotFoundException e) {
35             e.printStackTrace();
36         }
37         return obj;
38     }
39 }

通過對象序列化過程,把一個內存塊轉化為可傳輸的數據流,然後通過網絡發送到消息消費者(Customer)哪裡,進行反序列化,生成實驗對象,代碼如下:

1 public class Customer {
2     public static void main(String[] args) {
3         //反序列化
4         Person p=(Person) SerializationUtils.readObject();
5         System.out.println(p.getName());
6     }
7 }

這是一個反序列化的過程,也就是對象數據流轉換為一個實例的過程,其運行後的輸出結果為“混世魔王”。這太easy了,是的,這就是序列化和反序列化的典型Demo。但此處藏著一個問題:如果消息的生產者和消息的消費者(Person類)有差異,會出現何種神奇事件呢?比如:消息生產者中的Person類添加一個年齡屬性,而消費者沒有增加該屬性。為啥沒有增加?因為這個是分布式部署的應用,你甚至不知道這個應用部署在何處,特別是通過廣播方式發消息的情況,漏掉一兩個訂閱者也是很正常的。

  這中序列化和反序列化的類在不一致的情況下,反序列化時會報一個InalidClassException異常,原因是序列化和反序列化所對應的類版本發生了變化,JVM不能把數據流轉換為實例對象。刨根問底:JVM是根據什麼來判斷一個類的版本呢?

     好問題,通過SerializableUID,也叫做流標識符(Stream Unique Identifier),即類的版本定義的,它可以顯示聲明也可以隱式聲明。顯示聲明格式如下:

   private static final long serialVersionUID = 1867341609628930239L; 

  而隱式聲明則是我不聲明,你編譯器在編譯的時候幫我生成。生成的依據是通過包名、類名、繼承關系、非私有的方法和屬性,以及參數、返回值等諸多因子算出來的,極度復雜,基本上計算出來的這個值是唯一的。

  serialVersionUID如何生成已經說明了,我們再來看看serialVersionUID的作用。JVM在反序列化時,會比較數據流中的serialVersionUID與類的serialVersionUID是否相同,如果相同,則認為類沒有改變,可以把數據load為實例相同;如果不相同,對不起,我JVM不干了,拋個異常InviladClassException給你瞧瞧。這是一個非常好的校驗機制,可以保證一個對象即使在網絡或磁盤中“滾過”一次,仍能做到“出淤泥而不染”,完美的實現了類的一致性。

 但是,有時候我們需要一點特例場景,例如我的類改變不大,JVM是否可以把我以前的對象反序列化回來?就是依據顯示聲明的serialVersionUID,向JVM撒謊說"我的類版本沒有變化",如此我買你編寫的類就實現了向上兼容,我們修改Person類,裡面添加private static final long serialVersionUID = 1867341609628930239L;

  剛開始生產者和消費者持有的Person類一致,都是V1.0,某天生產者的Person類變更了,增加了一個“年齡”屬性,升級為V2.0,由於種種原因(比如程序員疏忽,升級時間窗口不同等)消費端的Person類還是V1.0版本,添加的代碼為 priavte int age;以及對應的setter和getter方法。

  此時雖然生產這和消費者對應的類版本不同,但是顯示聲明的serialVersionUID相同,序列化也是可以運行的,所帶來的業務問題就是消費端不能讀取到新增的業務屬性(age屬性而已)。通過此例,我們反序列化也實現了版本向上兼容的功能,使用V1.0版本的應用訪問了一個V2.0的對象,這無疑提高了代碼的健壯性。我們在編寫序列化類代碼時隨手添加一個serialVersionUID字段,也不會帶來太多的工作量,但它卻可以在關鍵時候發揮異乎尋常的作用。

  顯示聲明serialVersionUID可以避免對象的不一致,但盡量不要以這種方式向JVM撒謊。

建議12:避免用序列化類在構造函數中為不變量賦值

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

1 public class Person implements Serializable {
2     private static final long serialVersionUID = 1867341609628930239L;
3     public final String perName="程咬金";
4 }

  這個Peson類(此時V1.0版本)被序列化,然後存儲在磁盤上,在反序列化時perName屬性會重新計算其值(這與static變量不同,static變量壓根就沒有保存到數據流中)比如perName屬性修改成了"秦叔寶"(版本升級為V2.0),那麼反序列化的perName值就是"秦叔寶"。保持新舊對象的final變量相同,有利於代碼業務邏輯統一,這是序列化的基本原則之一,也就是說,如果final屬性是一個直接量,在反序列化時就會重新計算。對於基本原則不多說,現在說一下final變量的另一種賦值方式:通過構造函數賦值。代碼如下:

public class Person implements Serializable {
    private static final long serialVersionUID = 1867341609628930239L;
    public final String perName;

    public Person() {
        perName = "程咬金";
    }
}

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

public class Serialize {
    public static void main(String[] args) {
        //序列化以持久保持
        SerializationUtils.writeObject(new Person());
    }
}

Person的實習對象保存到了磁盤上,它時一個貧血對象(承載業務屬性定義,但不包含其行為定義),我們做一個簡單的模擬,修改一下PerName值代表變更,要注意的是serialVersionUID不變,修改後的代碼如下:

public class Person implements Serializable {
    private static final long serialVersionUID = 1867341609628930239L;
    public final String perName;

    public Person() {
        perName = "秦叔寶";
    }
}

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

public class Deserialize {
    public static void main(String[] args) {
        Person p = (Person) SerializationUtils.readObject();
        System.out.println(p.perName);
    }
}

現在問題出來了,打印出來的結果是"程咬金" 還是"秦叔寶"?答案是:"程咬金"。final類型的變量不是會重新計算嘛,打印出來的應該是秦叔寶才對呀。為什麼會是程咬金?這是因為這裡觸及到了反序列化的兩一個原則:反序列化時構造函數不會執行.

  反序列化的執行過程是這樣的:JVM從數據流中獲取一個Object對象,然後根據數據流中的類文件描述信息(在序列化時,保存到磁盤的對象文件中包含了類描述信息,注意是描述信息,不是類)查看,發現是final變量,需要重新計算,於是引用Person類中的perName值,而此時JVM又發現perName竟沒有賦值,不能引用,於是它很聰明的不再初始化,保持原值狀態,所以結果就是"程咬金"了。

  注意:在序列化類中不使用構造函數為final變量賦值.

建議13:避免為final變量復雜賦值

  為final變量賦值還有另外一種方式:通過方法賦值,及直接在聲明時通過方法的返回值賦值,還是以Person類為例來說明,代碼如下:

public class Person implements Serializable {
    private static final long serialVersionUID = 1867341609628930239L;
    //通過方法返回值為final變量賦值
    public final String pName = initName();

    public String initName() {
        return "程咬金";
    }
}

  pName屬性是通過initName方法的返回值賦值的,這在復雜的類中經常用到,這比使用構造函數賦值更簡潔,易修改,那麼如此用法在序列化時會不會有問題呢?我們一起看看。Person類寫好了(定義為V1.0版本),先把它序列化,存儲到本地文件,其代碼與之前相同,不在贅述。現在Person類的代碼需要修改,initName的返回值改為"秦叔寶".那麼我們之前存儲在磁盤上的的實例加載上來,pName的會是什麼呢?

  現在,Person類的代碼需要修改,initName的返回值也改變了,代碼如下: 

public class Person implements Serializable {
    private static final long serialVersionUID = 1867341609628930239L;
    //通過方法返回值為final變量賦值
    public final String pName = initName();

    public String initName() {
        return "秦叔寶";
    }
}

 上段代碼僅僅修改了initName的返回值(Person類為V2.0版本)也就是通過new生成的對象的final變量的值都是"秦叔寶",那麼我們把之前存儲在磁盤上的實例加載上來,pName的值會是什麼呢?

 結果是"程咬金",很詫異,上一建議說過final變量會被重新賦值,但是這個例子又沒有重新賦值,為什麼?

  上個建議說的重新賦值,其中的"值"指的是簡單對象。簡單對象包括:8個基本類型,以及數組、字符串(字符串情況復雜,不通過new關鍵字生成的String對象的情況下,final變量的賦值與基本類型相同),但是不能方法賦值。

  其中的原理是這樣的,保存到磁盤上(或網絡傳輸)的對象文件包括兩部分:

    (1).類描述信息:包括類路徑、繼承關系、訪問權限、變量描述、變量訪問權限、方法簽名、返回值、以及變量的關聯類信息。要注意一點是,它並不是class文件的翻版,它不記錄方法、構造函數、static變量等的具體實現。之所以類描述會被保存,很簡單,是因為能去也能回嘛,這保證反序列化的健壯運行。

    (2).非瞬態(transient關鍵字)和非靜態(static關鍵字)的實體變量值

      注意,這裡的值如果是一個基本類型,好說,就是一個簡單值保存下來;如果是復雜對象,也簡單,連該對象和關聯類信息一起保存,並且持續遞歸下去(關聯類也必須實現Serializable接口,否則會出現序列化異常),也就是遞歸到最後,還是基本數據類型的保存。

  正是因為這兩個原因,一個持久化的對象文件會比一個class類文件大很多,有興趣的讀者可以自己測試一下,體積確實膨脹了不少。  

  總結一下:反序列化時final變量在以下情況下不會被重新賦值:

建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題

  部分屬性持久化問題看似很簡單,只要把不需要持久化的屬性加上瞬態關鍵字(transient關鍵字)即可。這是一種解決方案,但有時候行不通。例如一個計稅系統和一個HR系統,通過RMI(Remote Method Invocation,遠程方法調用)對接,計稅系統需要從HR系統獲得人員的姓名和基本工資,以作為納稅的依據,而HR系統的工資分為兩部分:基本工資和績效工資,基本工資沒什麼秘密,績效工資是保密的,不能洩露到外系統,這明顯是連個相互關聯的類,先看看薪水類Salary的代碼:  

 1 public class Salary implements Serializable {
 2     private static final long serialVersionUID = 2706085398747859680L;
 3     // 基本工資
 4     private int basePay;
 5     // 績效工資
 6     private int bonus;
 7 
 8     public Salary(int _basepay, int _bonus) {
 9         this.basePay = _basepay;
10         this.bonus = _bonus;
11     }
12 //Setter和Getter方法略
13 
14 }

Person類和Salary類是關聯關系,代碼如下: 

 1 public class Person implements Serializable {
 2 
 3     private static final long serialVersionUID = 9146176880143026279L;
 4 
 5     private String name;
 6 
 7     private Salary salary;
 8 
 9     public Person(String _name, Salary _salary) {
10         this.name = _name;
11         this.salary = _salary;
12     }
13 
14     //Setter和Getter方法略
15 
16 }

這是兩個簡單的JavaBean,都實現了Serializable接口,具備了序列化的條件。首先計稅系統請求HR系統對一個Person對象進行序列化,把人員信息和工資信息傳遞到計稅系統中,代碼如下:  

 1 public class Serialize {
 2     public static void main(String[] args) {
 3         // 基本工資1000元,績效工資2500元
 4         Salary salary = new Salary(1000, 2500);
 5         // 記錄人員信息
 6         Person person = new Person("張三", salary);
 7         // HR系統持久化,並傳遞到計稅系統
 8         SerializationUtils.writeObject(person);
 9     }
10 }

在通過網絡傳輸到計稅系統後,進行反序列化,代碼如下:

 1 public class Deserialize {
 2     public static void main(String[] args) {
 3         Person p = (Person) SerializationUtils.readObject();
 4         StringBuffer buf = new StringBuffer();
 5         buf.append("姓名: "+p.getName());
 6         buf.append("\t基本工資: "+p.getSalary().getBasePay());
 7         buf.append("\t績效工資: "+p.getSalary().getBonus());
 8         System.out.println(buf);
 9     }
10 }

打印出的結果為:姓名: 張三    基本工資: 1000    績效工資: 2500

但是這不符合需求,因為計稅系統只能從HR系統中獲取人員姓名和基本工資,而績效工資是不能獲得的,這是個保密數據,不允許發生洩漏。怎麼解決這個問題呢?你可能會想到以下四種方案:

下面展示一個優秀的方案,其中實現了Serializable接口的類可以實現兩個私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。我們把Person類稍作修改,看看如何控制序列化和反序列化,代碼如下:

 1 public class Person implements Serializable {
 2 
 3     private static final long serialVersionUID = 9146176880143026279L;
 4 
 5     private String name;
 6 
 7     private transient Salary salary;
 8 
 9     public Person(String _name, Salary _salary) {
10         this.name = _name;
11         this.salary = _salary;
12     }
13     //序列化委托方法
14     private void writeObject(ObjectOutputStream oos) throws IOException {
15         oos.defaultWriteObject();
16         oos.writeInt(salary.getBasePay());
17     }
18     //反序列化委托方法
19     private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException {
20         input.defaultReadObject();
21         salary = new Salary(input.readInt(), 0);
22     }
23 }

其它代碼不做任何改動,運行之後結果為:姓名: 張三    基本工資: 1000    績效工資: 0

在Person類中增加了writeObject和readObject兩個方法,並且訪問權限都是私有級別,為什麼會改變程序的運行結果呢?其實這裡用了序列化的獨有機制:序列化回調。Java調用ObjectOutputStream類把一個對象轉換成數據流時,會通過反射(Refection)檢查被序列化的類是否有writeObject方法,並且檢查其是否符合私有,無返回值的特性,若有,則會委托該方法進行對象序列化,若沒有,則由ObjectOutputStream按照默認規則繼續序列化。同樣,在從流數據恢復成實例對象時,也會檢查是否有一個私有的readObject方法,如果有,則會通過該方法讀取屬性值,此處有幾個關鍵點需要說明:

分別是寫入和讀出相應的值,類似一個隊列,先進先出,如果此處有復雜的數據邏輯,建議按封裝Collection對象處理。大家可能注意到上面的方式也是Person失去了分布式部署的能了,確實是,但是HR系統的難點和重點是薪水的計算,特別是績效工資,它所依賴的參數很復雜(僅從數量上說就有上百甚至上千種),計算公式也不簡單(一般是引入腳本語言,個性化公式定制)而相對來說Person類基本上都是靜態屬性,計算的可能性不大,所以即使為性能考慮,Person類為分布式部署的意義也不大。

建議15:break萬萬不可忘

  我們經常會寫一些轉換類,比如貨幣轉換,日期轉換,編碼轉換等,在金融領域裡用到的最多的要數中文數字轉換了,比如把"1"轉換為"壹" ,不過開源工具是不會提供此工具類的,因為它太貼近中國文化了,需要自己編寫:

 1 public class Client15 {
 2     public static void main(String[] args) {
 3         System.out.println(toChineseNuberCase(0));
 4     }
 5 
 6     public static String toChineseNuberCase(int n) {
 7         String chineseNumber = "";
 8         switch (n) {
 9         case 0:
10             chineseNumber = "零";
11         case 1:
12             chineseNumber = "壹";
13         case 2:
14             chineseNumber = "貳";
15         case 3:
16             chineseNumber = "三";
17         case 4:
18             chineseNumber = "肆";
19         case 5:
20             chineseNumber = "伍";
21         case 6:
22             chineseNumber = "陸";
23         case 7:
24             chineseNumber = "柒";
25         case 8:
26             chineseNumber = "捌";
27         case 9:
28             chineseNumber = "玖";
29         }
30         return chineseNumber;
31     }
32 }

這是一個簡單的代碼,但運行結果卻是"玖",這個很簡單,可能大家在剛接觸語法時都學過,但雖簡單,如果程序員漏寫了,簡單的問題會造成很大的後果,甚至經濟上的損失。所以在用switch語句上記得加上break,養成良好的習慣。對於此類問題,除了平常小心之外,可以使用單元測試來避免,但大家都曉得,項目緊的時候,可能但單元測試都覆蓋不了。所以對於此類問題,一個最簡單的辦法就是:修改IDE的警告級別,例如在Eclipse中,可以依次點擊PerFormaces-->Java-->Compiler-->Errors/Warings-->Potential Programming problems,然後修改'switch' case fall-through為Errors級別,如果你膽敢不在case語句中加入break,那Eclipse直接就報個紅叉給你看,這樣可以避免該問題的發生了。但還是啰嗦一句,養成良好習慣更重要!

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved