序列化的一些注意事項及建議
本文來自《改善java的151個建議》
建議11:養成良好習慣,顯示宣告UID
我們先寫一個序列化與反序列化的工具類SerilizationUtils
public class SerializationUtils { private static String FILE_NAME="E:/serializable.txt"; public static void writeObject(Serializable s){ try{ ObjectOutputStream oob = new ObjectOutputStream(new FileOutputStream(FILE_NAME)); oob.writeObject(s); oob.close(); }catch(Exception e){}; } public static Object readObject(){ Object obj=null; try{ ObjectInputStream oob = new ObjectInputStream(new FileInputStream(FILE_NAME)); obj = oob.readObject(); oob.close(); }catch(Exception e){}; return obj; } }
Person類
public class Person implements Serializable { /** * 這裡先不顯示的宣告UID */ //private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } // /*private String sex; public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; }*/ }
首先定義一個訊息的產生者Produce
public class Produce {
public static void main(String[] args){
Person person = new Person();
person.setName("石仁闖");
//增加sex後
//person.setSex("男");
//序列化 儲存到磁碟上
SerializationUtils.writeObject(person);
System.out.println(person);
}
}
這裡執行了之後 person就被序列化到了E:/serializable.txt
然後定義一個消費者
public class Consumer {
public static void main(String[] args) throws Exception{
//反序列化
Person p = (Person)SerializationUtils.readObject();
System.out.println("反序列化得到的值"+p.getName());
//System.out.println("反序列化得到的值"+p.getSex());
}
}
好 執行到這裡是沒有什麼問題的;但是如果我們序列化與反序列化的時候參照的不是一個Person 會出現什麼情況 ;
比如我們在序列化之前person還是隻有一個屬性name 執行produce (序列化)之後;給person增加一個sex屬性(注意不要再執行produce (序列化)了);
增加了屬性之後 我們執行consumer(反序列化);會出現一個錯誤
書上說的是InvalidClassException錯誤;但是我親自執行報的是上面的錯誤;
為什麼會這樣呢?
原因是序列化與反序列化對應的類(person)版本不一致;JVM不能把資料流轉換為例項物件;
那JVM是怎麼判斷一個類的對應版本呢?
是通過SerivalVersionUID ,也就流識別符號,即類的版本定義
private static final long serialVersionUID = 1L;
UID可以隱式宣告和顯示宣告;隱式宣告是編譯器自動生成 基本上市唯一的;如果改變了類 ; 它是UID也會改變
在反序列化的時候 jvm會比較資料流中的UID與當前類(person)是否一致;如果一致說明類沒有改動;不一致說明是改動了,這是一個很好的效驗機制;
但是;有特殊情況;例如:我的類改變不大,我希望在反序列化的時候也能把它序列化出來。那怎麼辦呢?
既然是判斷UID是否一致,那我們讓他們的UID是一致的就可以了,顯示宣告UID 可以很好的解決這一問題;
建議12:避免用序列化類在建構函式中未不變數final賦值
private static final long serialVersionUID = 1L;
//final 常量是一個直接量 在反序列化的時候就會重新計算 保持final物件的新舊統一 有利於程式碼業務邏輯統一
public final String testFinal="序列化之前";
// public final String testFinal="序列化之後";
序列化的時候給testFinal賦值是 序列化之前
序列化完成之後 將testFinal改成 序列化之後
然後執行反序列化
//反序列化
Person p = (Person)SerializationUtils.readObject();
System.out.println("反序列化得到的值:"+p.testFinal);
輸出來的值是 序列化之前還是之後呢? 輸出結果是:反序列化得到的值:序列化之後
為什麼呢?我們在序列化的時候 將testFinal序列化成了資料流存在了磁碟中 按理說反序列化的時候 得到的也是 序列化之前的值啊 為什麼變成了序列化之後?
這是因為序列化的基本規則之一:保持final新舊物件的相同。有利於程式碼業務邏輯的統一;也就是說,如果final是一個直接量 則反序列化時候final就會被重新計算;
final變數的另外一種賦值:建構函式賦值
public final String testFinal;
public Person() {
// TODO Auto-generated constructor stub
testFinal="建構函式賦值 之前";
}
序列化之後修改
testFinal="建構函式賦值 之後";
然後進行反序列化;猜想:這裡輸出應該會是 建構函式賦值 之後 吧? 因為規則一也是這樣的啊!
那到底輸出什麼呢?
反序列化得到的值:建構函式賦值 之前
為什麼還是之前 而不是改變之後的呢? 原因是另一個規則
反序列化時建構函式不被執行!
建議13:避免為final變數複雜賦值
總結:反序列化在以下情況不能夠被重新賦值
1、通過建構函式為final變數賦值
2、通過方法未final變數賦值
3、final修飾的物件不是基本物件
建議14:使用序列化類的私有方法巧妙解決
部分屬性 持久化問題
......