對象序列化的目標:
- 將對的字節序列對象永久的保存到磁盤中。
- 允許在網絡上直接傳輸對象,傳輸對象的字節序列。
對象序列化:把對象轉換為字節序列。
對象反序列化:把字節序列恢復為對象。
如果讓某個對象支持序列化機制,必須讓它的類是可序列化的(serializable)。
為了讓某個類是可序列化的,該類必須實現如下兩個接口之一:
- Serializable
- Externalizablie
Externalizable接口繼承自 Serializable接口,僅實現Serializable接口的類采用默認的序列化方式 ,而實現Externalizable接口的類完全由自身來控制序列化的行為。
Java 的很多類已經實現了Serializable,該接口是一個標記接口,實現該接口無須實現任何方法,它只是表明該類的實例是可序列化的。
在 JavaEE 中,通常建議:創建的每個JavaBean類都實現Serializable。
關于對象序列化,需要注意的幾點:
- 對象的類名、實例變量都會被序列化;方法、類變量(static修飾的成員變量)、transient 修飾的實例變量都不會被序列化。
- 實現 Serializable 接口的類如果需要讓某個實例變量不被實例化,則在該實例變量前加 transient 修飾符。
- 要保證序列化對象的引用變量類型也是可序列化的。
- 反序列化對象時必須有序列化對象的 class 文件。
- 當通過文件、網絡來讀取序列化后的對象時,必須按實際寫入的順序讀取。
1、使用對象流來實現序列化
對象序列化的步驟:
1) 創建一個對象輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流;
2) 通過對象輸出流的writeObject()方法寫對象。
對象反序列化的步驟:
1) 創建一個對象輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流;
2) 通過對象輸入流的readObject()方法讀取對象。
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
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 String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
/**
* 實現序列化和反序列化
*
*/
public class SerializableTest {
public static void main(String[] args) {
Person person = new Person("小明", 23);
try {
SerializableObject(person);
Person p = (Person) DeserializationObject();
System.out.println(p.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 序列化對象
* @param o
* @throws Exception
*/
public static void SerializableObject(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(o);
System.out.println("對象序列化成功!");
oos.close();
}
/**
* 反序列化
* @return
* @throws Exception
*/
public static Object DeserializationObject() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Object o = ois.readObject();
System.out.println("反序列化成功!");
return o;
}
}
OK!
2、對象引用的序列化
前邊例子的Person
類的成員變量是String
,int
型的。
如果某個類的成員變量的類型不是基本類型或String類型,而是另一個引用類型,那么這個引用類必須是可序列化的,否則擁有該類型成員變量的類也是不可序列化的。
public class Teacher implements Serializable {
private String name;
private Person stuName;
}
Teacher類持有一個Person類的引用,只有Person類是可序列化的,Teacher類才是可序列化的。如果Person類不可序列化,則無論Teacher類是否實現了Serializable,Externalizable接口,Teacher類都是不可序列化的。
Java序列化機制采用了一種特殊的序列化算法:
- 所有保存到磁盤中的對象都有一個序列化編號。
- 當試圖序列化一個對象時,將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化過,系統才會將該對象轉換成字節序列并輸出。
- 如果某個對象已經被序列化過,程序將只輸出一個序列化編號,而不是再次序列化該對象。
注意:當使用Java序列化機制序列化可變對象時一定要注意,只有第一次調用 writeObject() 方法來輸出對象時才會將對象轉換成字節序列,并寫入到 ObjectOutputStream;在后面程序中即使該對象的實例變量發生了改變,再次調用writeObject() 方法輸出該對象時,改變后的實例變量也不會被輸出。
3、自定義序列化
有的時候,不希望系統將某一實例變量值進行序列化,或者某個實例變量的類型是不可序列化的。因此就不希望對該實例變量進行遞歸的序列化,以免引發NotSerializableException
異常。
對某個對象進行序列化時,系統會自動把該對象的所有實例變量依次進行序列化,如果某個實例變量引用到另一個對象,則被引用的對象也會被序列化。如果被引用的對象的實例變量也引用了其他對象,則被引用的對象也會被序列化,這種情況被稱為遞歸序列化。
解決辦法一:
在實例變量前面使用 transient
關鍵字修飾,可以指定Java序列化時無須理會該實例變量。
public class Teacher implements Serializable {
private transient String name;
private Person stuName;
}
transient
關鍵字只能用于修飾實例變量,不可修飾Java程序中的其他部分。
存在問題:被 transient
修飾的實例變量被完全隔離在序列化機制之外,將導致在反序列化時無法取得該實例變量的值。
解決辦法二:
利用Java提供的一種序列化機制,通過這種機制可以讓程序控制如何序列化各實例變量,甚至完全不序列化某些實例變量(與使用 transient
關鍵字的效果相同)。
在序列化和反序列化過程中,需要特殊處理的類應該重寫如下的特殊方法,來實現自定義序列化:
private void writeObject(ObjectOutputStream out) throws IOException // 可以自定義序列化機制
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException // 可以自定義反序列化機制
private void readObjectNoData() throws ObjectStreamException // 當序列化流不完整時,該方法可以用來正確地初始化反序列化的對象
注意:writeObject() 方法存儲實例變量的順序應該和readObject() 方法中恢復實例變量的順序一致,否則將不能正常恢復該Java對象。
解決辦法三:
一種更徹底的自定義機制,可以在序列化對象時將該對象替換成其他對象。讓序列化類重寫以下方法:
任何的訪問修飾符 Object writeReplace() throws ObjectStreamException
public class Teacher implements Serializable {
private String name;
private Person stuName;
// 重寫 writeReplace 方法,程序在序列化該對象之前,先調用該方法
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(stuName);
return list;
}
}
與之相對應的一個方法:
任何的訪問修飾符 Object readResolve() throws ObjectStreamException
該方法在緊接著readObject() 之后被調用,該方法的返回值將會代替原來反序列化的對象,而原來readObject()反序列化的對象將會被立即丟棄。
通常建議:對于 final 類重寫 readResolve() 方法不會有任何問題,否則,重寫 readResolve() 方法時應該盡量使用 private 修飾符。
反序列化機制在恢復Java對象時無須調用構造器來初始化Java對象,從這個意義上來看,序列化機制可以用來“克隆”對象。
4、另一種自定義序列化機制
這種序列化方式完全由程序員決定存儲和恢復對象數據。實現Externalizablie
接口,中的兩個方法:
void writeExternal(ObjectOutput out) // 實現序列化,調用ObjectOutput的writeObject()方法來保存引用類型的實例變量的值。
調用DataOutput(它是ObjectOutput的父接口)的方法來保存基本類型的實例變量的值。
void readExternal(ObjectInput in) // 實現反序列化,調用ObjectIutput的readObject()方法來恢復引用類型的實例變量的值。
調用DataIutput(它是ObjectIutput的父接口)的方法來恢復基本類型的實例變量的值。
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void writeExternal(ObjectOutput out) throws IOException {
//將name實例變量值反轉后寫入
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
由于編程復雜度的原因,大部分時候都是采用實現Serializable
接口的方式來實現序列化。
6、版本(serialVersionUID 的作用)
Java序列化機制允許為序列化類提供一個private static final 的 serialVersionUID 值,該類變量的值用于標識該Java類的序列化版本。
這樣就能保證即使在某個對象被序列化之后,它所對應的類被修改了,該對象也依然可以被正確的反序列化。避免在反序列化時與序列化版本的兼容性問題。