對象序列化
對象序列化的目標是將對象保存到磁盤中,或允許在網絡中直接傳輸對象,對象序列化機制運行把內存中的Java對象轉換成平臺無關的二進制流,從而允許把這個二進制流持久保存在磁盤上,通過網絡將這種二進制流傳輸到另一個網絡節點。其他程序一旦獲得了這個二進制流,都可以將這種二進制流恢復成原來的Java對象。
對象序列化將一個Java對象寫入IO流中,對象的反序列化則是從IO流中恢復原的該Java對象。
Java對象實現序列化的方式:
(1)實現Serializaable接口,該接口是一個標記接口,實現該接口無須實現任何方法,它只是表明該類的實例是可以序列化的。
(2)實現Externalizable接口,
使用對象流實現序列化
通過對象流實現序列化的步驟:
(1)創建一個ObjectOutputStream輸出流。
(2)調用OjbectOutputStream對象的writeObject方法輸出可序列化對象。
經典實例:
public class SerializableTest {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("D:\\test.txt"));
Person p = new Person("Android", 7);
// 將p對象寫入輸出流
oos.writeObject(p);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
反序列化的步驟:
(1)創建一個ObjectInputStream輸入流。
(2)調用ObjectInputStream對象的readObject方法讀取流中的對象。該方法返回一個Object類型的java對象。
經典實例:
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
//創建一個ObjectInputStream輸出流
ois = new ObjectInputStream(new FileInputStream("D:\\test.txt"));
//從流中讀取一個java對象
Person p = (Person) ois.readObject();
System.out.println("name=" + p.getName() + " ,age=" + p.getAge());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
反序列化機制無須通過構造器來初始化Java對象。如果反序列化某個子類實例時,反序列化機制需要恢復其關聯的父類實例,恢復這些父類實例有兩種方式:使用反序列化機制;使用父類無參數的構造函數。
如果一個可序列化類有多個父類(直接父類或間接父類),則該類的所有父類要麼是可序列化的,要麼有無參數的構造器,否則序列化時拋出IvaalidClassException異常。
對象引用的序列化
當程序序列化一個對象時,如果該對象持有一個其他對象的引用,爲了在反序列化時可以正常恢復該對象,則程序會順便把他引用的其他對象也進行序列化,否則該對象不能進行序列化。
Java序列化機制採用了一種特殊的序列化算法,其內容如下:
(1)所有保存到磁盤中的對象都有一個序列化編號。
(2)當程序試圖序列化一個對象時,程序將先檢測該對象是否已經序列化過,只有當該對象從未被序列化過,系統纔會將該對象轉換成字節序列並輸出。
(3)如果某個對象是已經序列化過的,程序將直接只輸出一個序列化編號,而不是再次重新序列化該對象。
注意:當使用Java序列化機制序列化可變對象時,一定要注意,只有當第一次調用wirteObject方法來輸出對象時纔會將對象轉換爲字節序列,並寫出到ObjectOutputStream;在後面程序中,如果該對象的屬性發送改變,即再次調用wirteObject方法輸出該對象時,改變後的屬性不會被輸出。
自定義序列化
遞歸序列化:當對某個對象進行序列化時,系統會自動把該對象的所有屬性依次進行序列化,如果某個屬性引用到另一個對象,則被引用的對象也會被序列化。如果被引用的對象的屬性也應用了其他對象,則被引用的對象也會被序列化。
通過在屬性前加transient關鍵字,可以指定Java序列化時無須理會該屬性,transient關鍵字只能用來修飾屬性,不可以修飾Java程序中其他部分。比如: private transient int age;
但使用transient關鍵字修飾屬性雖然很簡單方便,但被transient修飾的屬性將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java對象時無法獲得該屬性值。Java提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各屬性,甚至完全不序列化某些屬性。
自定義序列化需要對如下個方法進行處理
(1)private void writeObject(java.io.ObjectOutputStream out) throws IOException;
(2)private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
(3)private void readObjectNoData()throws ObjectStreamException;
writeObject方法負責寫入特定類的實例的狀態,以便相應的readObject方法可以恢復它。通過重寫該方法,程序員可以完全獲得對序列化機制的控制,程序員可以自主決定那些屬性需要序列化,需要怎樣序列化。默認情況下,該方法會調用out.defaultWriteObject來保存Java對象的各屬性,從而可以實現序列化Java對象狀態的目的。
readObject方法負責從流中讀取並恢復對象屬性,通過重寫該方法,程序員可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化那些屬性,以及進行怎樣的反序列化。默認情況下,該方法會調用in.defaultReadObject來恢復Java對象的非靜態屬性,通常情況下readObject方法與readObject對應,如果writeObject方法中對Java對象的屬性進行了一些處理,則應該在readObject方法中對該屬性進行相應的反處理,以便正確恢復該對象。
當序列化流不完整時,readObjectNoData方法可以用來正確地初始化反序列化的對象。
經典實例:
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
// TODO Auto-generated method stub
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
上面程序中與前面說的序列化和反序列化的區別是:序列化後的對象流,即使有Cracker獲取到Person對象流,他看到name將是我們加密後的name指,這樣就提高了序列化的安全性。注意,writeObject方法存儲屬性的順序應該和readObject方法中恢復屬性的順序一致,否則將不能正常恢復該Java對象。
還有一種更測底的自定義機制,它可以在序列化對象時將那個該對象替換成其他對象。
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此writeReplace方法將有序列化機制調用,前提是該方法存在。
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//從寫wirteReplace方法,程序在序列化該對象之前,先調用該方法
private Object wirteReplace() throws ObjectStreamException {
// TODO Auto-generated method stub
ArrayList<Object> list=new ArrayList<Object>();
list.add(name);
list.add(age);
return list;
}
}
我們知道系統在序列化某個對象之前,會先調用該對象的如下兩個方法:writeReplace和writeObject,系統總是先調用序列化對象的writeReplace方法,如果該方法返回另一個對象,系統將再次調用另一個對象的writeReplace方法.........直到該方法不再返回另一個對象爲止,最後程序調用該對象的writeObject方法來保存該對象的狀態。
與writeReplace方法相對的是,序列化機制還有一個特殊的方法,他可以實現保護性負責整個對象。
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
這個方法會緊接着readObject方法之後調用,該方法的返回值將會替代原來反序列化的對象,而原來readObject反序列化的對象將會被立即丟棄。
Externalizable自定義序列化機制
Java還提供了另一種序列化機制,這種序列化方式完全由程序員決定存儲和恢復對象數據,要實現該目的,java類必須實現Externalizable接口。
Externalizable接口定義的兩個方法:
(1)void readExternal(ObjectInput in):需要序列化的類實現readExternal方法來實現反序列化。該方法調用DataInput(它是ObjectInput的父接口)的方法來恢復基本類似的屬性值,調用ObjectInput的readObject方法來恢復引用類型的屬性值。
(2)void writeExternal(ObjectOutput out):需要序列化的類實現writeExternal方法來保存對象的狀態,該方法調用DataInput(它是ObjectInput的父接口)的方法來保存基本類型的屬性值,,調用ObjectOutput的writeObject方法來保存引用類型的屬性值。
經典實例:
public class Person implements Externalizable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
// TODO Auto-generated method stub
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
兩種序列化機制的對比:
實現Serializable接口 | 實現Externalizable接口 |
系統自動存儲必要的信息 | 程序員決定存儲那些信息 |
Java內建支持,易於實現,只需實現該接口即可 | 僅僅提供兩個空方法,實現該接口必須爲兩個空方法提供實現 |
性能略差 | 性能略高 |
總結:雖然實現Externalizable節誒可能帶來一定的性能提升,由於實現Externalizable接口導致了編程度的增加,所以大部分還是採用實現Serializable接口來實現序列化。
對象序列化注意事項:
(1)對象的類名、屬性(包括基本類型,數組,對其他對象的引用)都會被序列化;方法、static屬性(即靜態屬性)、transient屬性都不會被序列化。
(2)實現Serializable接口的類如果需要想讓某個屬性不被序列化,可以在該屬性前加transient修飾符,而不是加static關鍵字,雖然能達到效果,但static不能這樣使用。
(3)保證序列化對象的屬性的類型是可以序列化的,否則需要使用transient關鍵字來修飾該屬性,要不然,則該類是不可序列化的。
(4)反序列化對象時必須有序列化對象的class文件。
(5)當通過文件、網絡來讀取實例化後的對象時,必須按實際寫入的順序讀取。
版本
反序列化Java對象時必須提供該對象的class文件,如果項目升級,系統的class文件同樣也會升級。爲了避免升級後和升級前的兼容性。Java序列化機制允許序列化類提供一個private static final 的serialVersionUID屬性值,該屬性值用於標識該Java類的序列化版本,也就是說,如果升級後和升級前的serialVersionUID保持不變,序列化機制也會把它們當成同一個序列化版本。