引言
java序列化就是java類實作Serializable接口,然後利用ObjectOutputStream将java對象寫到二進制檔案中,然後将該二進制檔案通過網絡傳輸到其它java虛拟機環境中,再利用ObjectInputStream将該二進制檔案還原為java對象。本質上是将java虛拟機中生成的對象轉移到其它java虛拟機中使用。
前提條件
情境:兩個用戶端 A 和 B 試圖通過網絡傳遞對象資料,A 端将對象 C 序列化為二進制資料再傳給 B,B 反序列化得到 C。那麼要想序列化和反序列化成功需要具備以下條件: 1.A端和B端都要有C類,而且類路徑和功能代碼一緻。 2.C類實作Serailizable接口 3.C類的序列化id一緻。
序列化id的問題
java序列化機制是通過在運作時判斷類的序列化id(serialVersionUID)來驗證版本一緻性的。在進行反序列化時,jvm會把傳來的位元組流中的serialVersionUID與本地相應java類的serialVersionUID進行比較,如果相同就認為是一緻的,可以進行反序列化,否則就會出現序列化版本不緻的異常(InvalidClassException )。 1.不顯示設定id jdk文檔中有解釋,建議我們顯示聲明,因為如果不聲明,Java序列化機制會根據編譯的class自動生成一個serialVersionUID,這種情況下,隻有同一次編譯生成的class才會生成相同的serialVersionUID,這樣的話很容易導緻序列化版本不一緻的異常。 2.顯示設定id 在eclipse中顯示設定序列化id有兩種方式: 1.add default serial version ID: 生成1L,對于這一種,需要了解哪些情況是相容的,哪些是不相容的,在可相容的前提下,可以保留舊版本号,如果不相容,或者想讓它不相容,就手工遞增版本号。1》2》3 2.add generated serial version ID: 生成一個很大的數,這種方式是根據類的結構産生的hash值,增減屬性、方法等,都可能會導緻這個值産生變化。如果開發人員認為每次修改類後都需要生成新的版本号,不想向下相容,操作就是删除原有的SerialVersionUid聲明語句,再自動生成一下。
靜态化變量序列化
java在序列化時,并不儲存靜态變量資訊,這其實比較容易了解,序列化儲存的是對象的狀态,靜态變量屬于類的狀态。
Transient關鍵字
Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到檔案中,在被反序列化後,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。
父類的序列化
情境:一個子類實作了Serializable接口,它的父類沒有實作,序列化該子類對象,然後反序列化輸出父類定義的變量的數值,該變量數值跟Transient關鍵字的效果一樣,都是初始值。 解決:要想将父類對象的屬性也序列化,就需要讓父類也實作Serializalbe接口。 如果父類不實作序列化,就需要有預設的無參的構造函數。在父類沒有實作Serializable接口時,虛拟機不會序列化父類對象,而一個java對象的構造必須先有父類對象,反序列化時也不例外。是以反序列化時,為了構造父對象,隻能調用父類的無參構造函數作為預設的父對象。如果你考慮到這種序列化的情況,在父類的無參構造函數中對變量進行初始化,否則的話,父類變量值都是預設聲明的值,如 int 型的是 0,對象型的是 null。 結論:根據以上, Transient 關鍵字可以使得字段不被序列化,那麼還有别的方法嗎?根據父類對象序列化的規則,我們可以将不需要被序列化的字段抽取出來放到父類中,子類實作 Serializable 接口,父類不實作,根據父類序列化規則,父類的字段資料将不被序列化。
定制序列化
原理:在序列化過程中,虛拟機會試圖調用對象類裡面的writeObject(private)和readObject(private)方法,進行使用者自定義的序列化和反序列化,如果沒有定義該方法,則預設調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。使用者自定義的 writeObject 和 readObject 方法可以允許使用者控制序列化的過程,當然使用者自定義了writeObject也可以調用defaultWriteObject來處理預設的序列化方法。 關于對象裡面的writeObject和readObject必須是private類型的,這個嗎,ObjectOutputStream使用了反射來尋找是否聲明了這兩個方法,因為ObjectOutputStream使用getPrivateMethod,是以這些方法不得不被聲明為priate以至于供ObjectOutputStream來使用。 定制序列化的過程有兩種試,一種是key,value型,一種是value型。 第一種: 序列化:key是對象中必須存在的字段的名稱,value是指定的值。
private void writeObject(ObjectOutputStream oos) {
try {
PutField putField = oos.putFields();
putField.put("name", "bao");
putField.put("name2", "elva");
oos.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
反序列化:key是寫的時候的值,不一定是類中必須存在的
private void readObject(ObjectInputStream ois) {
try{
GetField getField = ois.readFields();
String name = (String)getField.get("name", "");
System.out.println(name);
String name2 = (String)getField.get("name2", "");
System.out.println(name2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
第二種: 序列化:依次寫入不同字段的值,這裡隻有值,沒有字段名(key)
private void writeObject(ObjectOutputStream oos) {
try {
oos.defaultWriteObject(); // 執行預設的序列化過程
oos.writeObject("aaaaaaa"); // 添加值aaaaaa
oos.writeObject("bbbbbbb");// 添加值bbbbbb
} catch (IOException e) {
e.printStackTrace();
}
}
反序列化:依昭序列化時的順序和個數擷取相應的字段值
private void readObject(ObjectInputStream ois) {
try{
ois.defaultReadObject();
Object obj1 = ois.readObject();
Object obj2 = ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
使用場景:如果序列化的對象資料中,有些資料是敏感的,比如密碼字元串等,希望對密碼字段在序列化時,進行加密,在反序列化時,對密碼字段進行解密,這樣一定程式上保證序列化對象的資料安全。
序列化存儲規則
原理:當序列化同一個對象多次時,序列化存儲的二進制檔案中該對象隻存一份,剩下的全部存引用,這樣可以減少存儲空間。如果序列化同一個對象多次,當反序列化時獲得的對象是同一個,都指向同一個内在位址。 序列化同一個對象多次時,存儲的二進制檔案空間并沒有增加2倍,而隻增加一點點(存儲對象位址的那部分空間):
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(new File("D:/serializable.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
SerializableEntity entity = new SerializableEntity("elva", "29", "151********");
oos.writeObject(entity);
oos.flush();
System.out.println(new File("D:/serializable.txt").length()); // 126
oos.writeObject(entity);
oos.flush();
System.out.println(new File("D:/serializable.txt").length()); // 131
oos.close();
fos.close();
}
反序列化時:
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream(new File("D:/serializable.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
SerializableEntity entity = (SerializableEntity)ois.readObject();
SerializableEntity entity2 = (SerializableEntity)ois.readObject();
System.out.println(entity == entity2); // true
ois.close();
fis.close();
}
如果序列化多次同一個對象的過程中修改了該對象的某個字段值,那麼反序列化時擷取的對象的值保持不變:因為序列化多次同一個對象隻會序列化一次對象資料,剩下的是引用。
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(new File("D:/serializable.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
SerializableEntity entity = new SerializableEntity("elva", "29", "151********");
oos.writeObject(entity);
oos.flush();
System.out.println(new File("D:/serializable.txt").length()); // 126
entity.setName("bao");
oos.writeObject(entity);
oos.flush();
System.out.println(new File("D:/serializable.txt").length()); // 131
oos.close();
fos.close();
}
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream(new File("D:/serializable.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
SerializableEntity entity = (SerializableEntity)ois.readObject();
System.out.println(entity.getName()); // elva
SerializableEntity entity2 = (SerializableEntity)ois.readObject();
System.out.println(entity.getName()); // elva
System.out.println(entity == entity2); // true
ois.close();
fis.close();
}
參考: http://blog.csdn.net/jiangwei0910410003/article/details/18989711/ http://blog.csdn.net/mashangyou/article/details/21827613